// Targeted-kill tests for surviving mutants in WPParticleParser.cpp.
// Each TEST_SUITE / TEST_CASE pins a specific surviving operator/initializer
// branch with concrete numeric expectations so common arithmetic / comparison
// mutations (cxx_lt_to_le, cxx_mul_to_div, cxx_sub_to_add, cxx_gt_to_ge, ...)
// produce observable behaviour drift.
//
// Pattern note: every fixture method that takes a particle is given a fully
// pre-allocated `std::vector<Particle>` and uses INDEX access (not references)
// to avoid push_back-invalidation bugs.  Every test uses a fresh local CP
// span backed by a stack array, so no shared state crosses test boundaries.

#include <doctest.h>

#include "WPParticleParser.hpp"
#include "Particle/Particle.h"
#include "Particle/ParticleEmitter.h"
#include "Particle/ParticleModify.h"
#include "wpscene/WPParticleObject.h"

#include <Eigen/Core>
#include <array>
#include <cmath>
#include <nlohmann/json.hpp>
#include <span>
#include <vector>

using namespace wallpaper;
using nlohmann::json;

namespace deepkill_helpers
{

// Build a fresh particle.  No fixture struct — pure value semantics avoid
// reference-invalidation bugs.
inline Particle makeParticle() {
    Particle p;
    p.lifetime      = 1.0f;
    p.init.lifetime = 1.0f;
    p.alpha         = 1.0f;
    p.init.alpha    = 1.0f;
    p.size          = 20.0f;
    p.init.size     = 20.0f;
    p.color         = Eigen::Vector3f(1, 1, 1);
    p.init.color    = p.color;
    p.position      = Eigen::Vector3f(0, 0, 0);
    p.velocity      = Eigen::Vector3f(0, 0, 0);
    p.rotation      = Eigen::Vector3f(0, 0, 0);
    p.angularVelocity = Eigen::Vector3f(0, 0, 0);
    p.random_seed   = 0xdeadbeef;
    return p;
}

inline wpscene::ParticleInstanceoverride emptyOverride() {
    wpscene::ParticleInstanceoverride o;
    o.enabled = false;
    return o;
}

// Build a default 8-CP zero-resolved span backed by an out-parameter vector.
inline std::span<ParticleControlpoint> defaultCps(std::vector<ParticleControlpoint>& storage) {
    storage.assign(8, ParticleControlpoint {});
    for (auto& cp : storage) {
        cp.resolved = Eigen::Vector3d(0, 0, 0);
        cp.velocity = Eigen::Vector3d(0, 0, 0);
    }
    return std::span<ParticleControlpoint>(storage.data(), storage.size());
}

// Run a single-particle operator: build vectors, call the lambda, return the
// modified particle.
inline Particle runOpSingle(ParticleOperatorOp op, const Particle& base,
                            double time_pass = 0.016, double time = 0.0) {
    std::vector<Particle> ps; ps.reserve(8); ps.push_back(base);
    std::vector<ParticleControlpoint> cps_storage;
    auto cps = defaultCps(cps_storage);
    ParticleInfo info {
        .particles     = std::span<Particle>(ps),
        .controlpoints = cps,
        .time          = time,
        .time_pass     = time_pass,
    };
    op(info);
    return ps[0];
}

inline Particle runOpSingleCps(ParticleOperatorOp op, const Particle& base,
                               std::span<ParticleControlpoint> cps,
                               double time_pass = 0.016, double time = 0.0) {
    std::vector<Particle> ps; ps.reserve(8); ps.push_back(base);
    ParticleInfo info {
        .particles     = std::span<Particle>(ps),
        .controlpoints = cps,
        .time          = time,
        .time_pass     = time_pass,
    };
    op(info);
    return ps[0];
}

} // namespace deepkill_helpers

using namespace deepkill_helpers;

// ============================================================================
// remapvalue operator — surviving mutants in transform fns, octave clamp,
// timeofday, controlpoint outputs, output writes (~46 mutants)
// ============================================================================

TEST_SUITE("remapvalue transform fns deep") {
    static auto runRemap(const json& j, double time = 0.0, double time_pass = 0.016) {
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.size = 0.0f;
        return runOpSingle(op, p, time_pass, time);
    }

    TEST_CASE("transformfunction sine peaks at xs=0.25 and troughs at xs=0.75") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlesystemtime" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "transformfunction", "sine" }, { "transforminputscale", 1.0 },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 1.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        // sine: peak at t=0.25 (tx=1.0 → size=100), trough at t=0.75 (tx=0.0 → size=0).
        CHECK(runRemap(base, 0.25).size > 90.0f);
        CHECK(runRemap(base, 0.75).size < 10.0f);
    }

    TEST_CASE("transformfunction cosine peaks at xs=0 and troughs at xs=0.5") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlesystemtime" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "transformfunction", "cosine" }, { "transforminputscale", 1.0 },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 1.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        CHECK(runRemap(base, 0.0).size > 90.0f);
        CHECK(runRemap(base, 0.5).size < 10.0f);
    }

    TEST_CASE("transformfunction square gives 1.0 then 0.0 across half-period") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlesystemtime" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "transformfunction", "square" }, { "transforminputscale", 1.0 },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 1.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        CHECK(runRemap(base, 0.25).size == doctest::Approx(100.0f));
        CHECK(runRemap(base, 0.75).size == doctest::Approx(0.0f));
    }

    TEST_CASE("transformfunction saw rises linearly: tx = xs - floor(xs)") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlesystemtime" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "transformfunction", "saw" }, { "transforminputscale", 1.0 },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 1.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        CHECK(runRemap(base, 0.25).size == doctest::Approx(25.0f).epsilon(0.05));
        CHECK(runRemap(base, 0.5).size == doctest::Approx(50.0f).epsilon(0.05));
        CHECK(runRemap(base, 0.99).size == doctest::Approx(99.0f).epsilon(0.05));
    }

    TEST_CASE("transformfunction triangle: |2*frac-1|") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlesystemtime" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "transformfunction", "triangle" }, { "transforminputscale", 1.0 },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 1.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        // t=0 → 1; t=0.5 → 0; t=0.25 → 0.5.
        CHECK(runRemap(base, 0.0).size == doctest::Approx(100.0f).epsilon(0.05));
        CHECK(runRemap(base, 0.5).size == doctest::Approx(0.0f).epsilon(0.05));
        CHECK(runRemap(base, 0.25).size == doctest::Approx(50.0f).epsilon(0.05));
    }

    TEST_CASE("transformfunction step is floor(xs); scale tunes period") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlesystemtime" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "transformfunction", "step" },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 1.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        json b1 = base; b1["transforminputscale"] = 1.0;
        json b2 = base; b2["transforminputscale"] = 2.0;
        // scale=1 at t=0.5 → xs=0.5 → floor(0.5)=0 → size=0.
        CHECK(runRemap(b1, 0.5).size == doctest::Approx(0.0f));
        // scale=2 at t=0.5 → xs=1.0 → floor(1.0)=1 → size=100.
        CHECK(runRemap(b2, 0.5).size == doctest::Approx(100.0f));
    }

    TEST_CASE("transformfunction smoothstep applies cubic 3t^2-2t^3") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlesystemtime" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "transformfunction", "smoothstep" }, { "transforminputscale", 1.0 },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 1.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        // smoothstep(0.5) = 0.5*0.5*(3 - 1) = 0.5 → size=50.
        CHECK(runRemap(base, 0.5).size == doctest::Approx(50.0f).epsilon(0.05));
    }

    TEST_CASE("linear/unknown transform: identity pass-through scaled") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlesystemtime" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "transformfunction", "linear" }, { "transforminputscale", 0.5 },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 1.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        // tx = xs = 0.5 * 0.5 = 0.25 → size=25.
        CHECK(runRemap(base, 0.5).size == doctest::Approx(25.0f).epsilon(0.05));
    }

    TEST_CASE("transformoctaves clamped to [1,8] for fbmnoise") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlesystemtime" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "transformfunction", "fbmnoise" },
                      { "transforminputscale", 1.0 },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 1.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        for (int oct : { -10, 0, 100 }) {
            json j = base; j["transformoctaves"] = oct;
            float v = runRemap(j, 0.3).size;
            CHECK(std::isfinite(v));
            CHECK(v >= 0.0f);
            CHECK(v <= 100.0f);
        }
    }

    TEST_CASE("input timeofday yields fraction in [0, 1]") {
        json base = { { "name", "remapvalue" },
                      { "input", "timeofday" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 1.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        float v = runRemap(base).size;
        CHECK(v >= 0.0f);
        CHECK(v <= 100.0f);
    }

    TEST_CASE("input particlerotation reduces to z component") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlerotation" }, { "inputcomponent", "z" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 10.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        auto p = makeParticle();
        p.rotation = Eigen::Vector3f(1, 2, 5);
        auto out = runOpSingle(op, p);
        CHECK(out.size == doctest::Approx(50.0f).epsilon(0.05));
    }

    TEST_CASE("input particleangularvelocity reduces via inputcomponent") {
        json base = { { "name", "remapvalue" },
                      { "input", "particleangularvelocity" }, { "inputcomponent", "y" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 10.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        auto p = makeParticle();
        p.angularVelocity = Eigen::Vector3f(1, 5, 9);
        auto out = runOpSingle(op, p);
        CHECK(out.size == doctest::Approx(50.0f).epsilon(0.05));
    }

    TEST_CASE("input particleposition reduces via component") {
        json base = { { "name", "remapvalue" },
                      { "input", "particleposition" }, { "inputcomponent", "x" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 10.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(7, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.size == doctest::Approx(70.0f).epsilon(0.05));
    }

    TEST_CASE("input particlecolor reduces via component") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlecolor" }, { "inputcomponent", "x" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 1.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        auto p = makeParticle();
        p.color = Eigen::Vector3f(0.4f, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.size == doctest::Approx(40.0f).epsilon(0.05));
    }

    TEST_CASE("input maxlifetime returns init.lifetime") {
        json base = { { "name", "remapvalue" },
                      { "input", "maxlifetime" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 4.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        auto p = makeParticle();
        p.init.lifetime = 2.0f;
        auto out = runOpSingle(op, p);
        CHECK(out.size == doctest::Approx(50.0f).epsilon(0.05));
    }

    TEST_CASE("input runtime/layertime/particlesystemtime resolve to info.time") {
        for (const char* in : { "runtime", "layertime", "particlesystemtime" }) {
            json j  = { { "name", "remapvalue" },
                        { "input", in },
                        { "output", "particlesize" }, { "operation", "set" },
                        { "inputrangemin", 0.0 }, { "inputrangemax", 10.0 },
                        { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
            auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
            auto p = makeParticle();
            auto out = runOpSingle(op, p, 0.016, 5.0);
            CHECK(out.size == doctest::Approx(50.0f).epsilon(0.05));
        }
    }

    TEST_CASE("input controlpointvelocity reads CP velocity reduced via component") {
        json base = { { "name", "remapvalue" },
                      { "input", "controlpointvelocity" }, { "inputcontrolpoint0", 0 },
                      { "inputcomponent", "x" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 10.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        std::vector<ParticleControlpoint> cps;
        auto cp_span = defaultCps(cps);
        cps[0].velocity = Eigen::Vector3d(8, 0, 0);
        auto p = makeParticle();
        p.size = 0.0f;
        auto out = runOpSingleCps(op, p, cp_span);
        CHECK(out.size == doctest::Approx(80.0f).epsilon(0.05));
    }

    TEST_CASE("input deltatocontrolpoint reads particle minus CP, reduced via component") {
        json base = { { "name", "remapvalue" },
                      { "input", "deltatocontrolpoint" }, { "inputcontrolpoint0", 0 },
                      { "inputcomponent", "y" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 10.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        std::vector<ParticleControlpoint> cps;
        auto cp_span = defaultCps(cps);
        auto p = makeParticle();
        p.position = Eigen::Vector3f(0, 6, 0);
        auto out = runOpSingleCps(op, p, cp_span);
        CHECK(out.size == doctest::Approx(60.0f).epsilon(0.05));
    }

    TEST_CASE("input directiontocontrolpoint normalises and reduces") {
        json base = { { "name", "remapvalue" },
                      { "input", "directiontocontrolpoint" }, { "inputcontrolpoint0", 0 },
                      { "inputcomponent", "x" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "inputrangemin", -1.0 }, { "inputrangemax", 1.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        std::vector<ParticleControlpoint> cps;
        auto cp_span = defaultCps(cps);
        cps[0].resolved = Eigen::Vector3d(10, 0, 0);
        auto p = makeParticle();
        // Direction toward CP=+x → reduce x → 1 → t=1 → size=100.
        auto out = runOpSingleCps(op, p, cp_span);
        CHECK(out.size == doctest::Approx(100.0f).epsilon(0.05));
    }

    TEST_CASE("input directiontocontrolpoint particle at CP returns 0 (not NaN)") {
        json base = { { "name", "remapvalue" },
                      { "input", "directiontocontrolpoint" }, { "inputcontrolpoint0", 0 },
                      { "inputcomponent", "x" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "inputrangemin", -1.0 }, { "inputrangemax", 1.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        std::vector<ParticleControlpoint> cps;
        auto cp_span = defaultCps(cps);
        auto p = makeParticle();
        auto out = runOpSingleCps(op, p, cp_span);
        CHECK(std::isfinite(out.size));
    }

    TEST_CASE("input positionbetweentwocontrolpoints projects between two CPs") {
        json base = { { "name", "remapvalue" },
                      { "input", "positionbetweentwocontrolpoints" }, { "inputcontrolpoint0", 0 },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 1.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        std::vector<ParticleControlpoint> cps;
        auto cp_span = defaultCps(cps);
        cps[1].resolved = Eigen::Vector3d(100, 0, 0);
        auto p = makeParticle();
        p.position = Eigen::Vector3f(50, 0, 0);
        auto out = runOpSingleCps(op, p, cp_span);
        CHECK(out.size == doctest::Approx(50.0f).epsilon(0.05));
    }

    TEST_CASE("input random produces finite output in [0,100]") {
        json base = { { "name", "remapvalue" },
                      { "input", "random" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 1.0 },
                      { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 } };
        auto out = runRemap(base);
        CHECK(std::isfinite(out.size));
        CHECK(out.size >= 0.0f);
        CHECK(out.size <= 100.0f);
    }

    TEST_CASE("input layerorigin always reads 0 (warning logged once)") {
        json base = { { "name", "remapvalue" },
                      { "input", "layerorigin" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "inputrangemin", 0.0 }, { "inputrangemax", 1.0 },
                      { "outputrangemin", 17.0 }, { "outputrangemax", 99.0 } };
        // raw=0 → t=0 → mapped = outMin = 17.
        CHECK(runRemap(base).size == doctest::Approx(17.0f).epsilon(0.05));
    }

    TEST_CASE("subtract operation deducts mapped value (no blend window)") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlelifetime" },
                      { "output", "particlesize" },
                      { "operation", "subtract" },
                      { "outputrangemin", 3.0 }, { "outputrangemax", 3.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        auto p = makeParticle();
        p.size = 10.0f;
        auto out = runOpSingle(op, p);
        CHECK(out.size == doctest::Approx(7.0f).epsilon(0.05));
    }

    TEST_CASE("output alpha alias short-form") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlelifetime" },
                      { "output", "alpha" }, { "operation", "set" },
                      { "outputrangemin", 0.5 }, { "outputrangemax", 0.5 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        auto p = makeParticle();
        p.alpha = 1.0f;
        auto out = runOpSingle(op, p);
        CHECK(out.alpha == doctest::Approx(0.5f));
    }

    TEST_CASE("output rotation full-vec write applies to all components") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlelifetime" },
                      { "output", "rotation" }, { "operation", "set" },
                      { "outputrangemin", 1.0 }, { "outputrangemax", 1.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        auto p = makeParticle();
        p.rotation = Eigen::Vector3f(0, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.rotation.x() == doctest::Approx(1.0f).epsilon(0.05));
        CHECK(out.rotation.y() == doctest::Approx(1.0f).epsilon(0.05));
        CHECK(out.rotation.z() == doctest::Approx(1.0f).epsilon(0.05));
    }

    TEST_CASE("output controlpoint x writes back into CP resolved") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlelifetime" },
                      { "output", "controlpoint" }, { "outputcomponent", "x" },
                      { "outputcontrolpoint0", 0 }, { "operation", "set" },
                      { "outputrangemin", 42.0 }, { "outputrangemax", 42.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        std::vector<ParticleControlpoint> cps;
        auto cp_span = defaultCps(cps);
        auto p = makeParticle();
        std::vector<Particle> ps; ps.reserve(8); ps.push_back(p);
        ParticleInfo info {
            .particles     = std::span<Particle>(ps),
            .controlpoints = cp_span,
            .time          = 0.0,
            .time_pass     = 0.016,
        };
        op(info);
        CHECK(cps[0].resolved.x() == doctest::Approx(42.0));
        CHECK(cps[0].resolved.y() == doctest::Approx(0.0));
    }

    TEST_CASE("output controlpoint full-vec writes broadcast to all axes") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlelifetime" },
                      { "output", "controlpoint" },
                      { "outputcontrolpoint0", 0 }, { "operation", "set" },
                      { "outputrangemin", 5.0 }, { "outputrangemax", 5.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        std::vector<ParticleControlpoint> cps;
        auto cp_span = defaultCps(cps);
        auto p = makeParticle();
        std::vector<Particle> ps; ps.reserve(8); ps.push_back(p);
        ParticleInfo info {
            .particles     = std::span<Particle>(ps),
            .controlpoints = cp_span,
            .time          = 0.0,
            .time_pass     = 0.016,
        };
        op(info);
        CHECK(cps[0].resolved.x() == doctest::Approx(5.0));
        CHECK(cps[0].resolved.y() == doctest::Approx(5.0));
        CHECK(cps[0].resolved.z() == doctest::Approx(5.0));
    }

    TEST_CASE("set operation no blend window → full set") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlelifetime" },
                      { "output", "particlesize" }, { "operation", "set" },
                      { "outputrangemin", 99.0 }, { "outputrangemax", 99.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        auto p = makeParticle();
        p.size = 7.0f;
        auto out = runOpSingle(op, p);
        CHECK(out.size == doctest::Approx(99.0f));
    }

    TEST_CASE("multiply operation: blend=1 → multiply by op_val") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlelifetime" },
                      { "output", "particlesize" }, { "operation", "multiply" },
                      { "outputrangemin", 4.0 }, { "outputrangemax", 4.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        auto p = makeParticle();
        p.size = 3.0f;
        auto out = runOpSingle(op, p);
        // 3 * (1 + (4-1)*1) = 12.
        CHECK(out.size == doctest::Approx(12.0f));
    }

    TEST_CASE("add operation: current + op_val * blend") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlelifetime" },
                      { "output", "particlesize" }, { "operation", "add" },
                      { "outputrangemin", 7.0 }, { "outputrangemax", 7.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        auto p = makeParticle();
        p.size = 10.0f;
        auto out = runOpSingle(op, p);
        // 10 + 7*1 = 17.
        CHECK(out.size == doctest::Approx(17.0f));
    }

    TEST_CASE("subtract operation: current - op_val * blend") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlelifetime" },
                      { "output", "particlesize" }, { "operation", "subtract" },
                      { "outputrangemin", 4.0 }, { "outputrangemax", 4.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        auto p = makeParticle();
        p.size = 10.0f;
        auto out = runOpSingle(op, p);
        // 10 - 4*1 = 6.
        CHECK(out.size == doctest::Approx(6.0f));
    }

    TEST_CASE("output velocity x with set: replaces only x component") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlelifetime" },
                      { "output", "particlevelocity" }, { "outputcomponent", "x" },
                      { "operation", "set" },
                      { "outputrangemin", 99.0 }, { "outputrangemax", 99.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        auto p = makeParticle();
        p.velocity = Eigen::Vector3f(1, 2, 3);
        auto out = runOpSingle(op, p);
        CHECK(out.velocity.x() == doctest::Approx(99.0f));
        CHECK(out.velocity.y() == doctest::Approx(2.0f)); // unchanged
        CHECK(out.velocity.z() == doctest::Approx(3.0f)); // unchanged
    }

    TEST_CASE("output color full-vec write applies to all xyz") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlelifetime" },
                      { "output", "particlecolor" },
                      { "operation", "set" },
                      { "outputrangemin", 0.5 }, { "outputrangemax", 0.5 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        auto p = makeParticle();
        p.color = Eigen::Vector3f(1, 1, 1);
        auto out = runOpSingle(op, p);
        CHECK(out.color.x() == doctest::Approx(0.5f));
        CHECK(out.color.y() == doctest::Approx(0.5f));
        CHECK(out.color.z() == doctest::Approx(0.5f));
    }

    TEST_CASE("output angularvelocity full-vec write applies to all xyz") {
        json base = { { "name", "remapvalue" },
                      { "input", "particlelifetime" },
                      { "output", "particleangularvelocity" },
                      { "operation", "set" },
                      { "outputrangemin", 1.0 }, { "outputrangemax", 1.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        auto p = makeParticle();
        p.angularVelocity = Eigen::Vector3f(0, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.angularVelocity.x() == doctest::Approx(1.0f));
        CHECK(out.angularVelocity.y() == doctest::Approx(1.0f));
        CHECK(out.angularVelocity.z() == doctest::Approx(1.0f));
    }
}

// ============================================================================
// mapsequencebetweencontrolpoints — surviving 40 mutants
// ============================================================================

TEST_SUITE("mapsequencebetweencontrolpoints deep") {
    static auto cpSpanInline(std::vector<ParticleControlpoint>& storage) {
        return std::span<const ParticleControlpoint>(storage.data(), storage.size());
    }

    TEST_CASE("count=2 places particles at endpoints") {
        std::vector<ParticleControlpoint> cps(8, ParticleControlpoint {});
        for (auto& cp : cps) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        cps[1].resolved = Eigen::Vector3d(100, 0, 0);
        json j = { { "name", "mapsequencebetweencontrolpoints" },
                   { "count", 2 },
                   { "controlpointstart", 0 }, { "controlpointend", 1 },
                   { "arcamount", 0.0 }, { "sizereductionamount", 0.0 } };
        auto init = WPParticleParser::genParticleInitOp(j, cpSpanInline(cps));
        Particle p0 = makeParticle(); init(p0, 0.0); // idx=0, t=0 → cp0
        Particle p1 = makeParticle(); init(p1, 0.0); // idx=1, t=1 → cp1
        CHECK(p0.position.x() == doctest::Approx(0.0f).epsilon(0.05));
        CHECK(p1.position.x() == doctest::Approx(100.0f).epsilon(0.05));
    }

    TEST_CASE("size reduction shrinks last particle to (1-amount)") {
        std::vector<ParticleControlpoint> cps(8, ParticleControlpoint {});
        for (auto& cp : cps) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        cps[1].resolved = Eigen::Vector3d(100, 0, 0);
        json j = { { "name", "mapsequencebetweencontrolpoints" },
                   { "count", 2 },
                   { "controlpointstart", 0 }, { "controlpointend", 1 },
                   { "sizereductionamount", 0.5 } };
        auto init = WPParticleParser::genParticleInitOp(j, cpSpanInline(cps));
        Particle p0 = makeParticle(); p0.size = 10.0f; init(p0, 0.0); // t=0 → scale=1
        Particle p1 = makeParticle(); p1.size = 10.0f; init(p1, 0.0); // t=1 → scale=0.5 → size=5
        CHECK(p0.size == doctest::Approx(10.0f).epsilon(0.05));
        CHECK(p1.size == doctest::Approx(5.0f).epsilon(0.05));
    }

    TEST_CASE("size reduction with amount > 1 clamps scale to 0") {
        std::vector<ParticleControlpoint> cps(8, ParticleControlpoint {});
        for (auto& cp : cps) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        cps[1].resolved = Eigen::Vector3d(100, 0, 0);
        json j = { { "name", "mapsequencebetweencontrolpoints" },
                   { "count", 2 },
                   { "controlpointstart", 0 }, { "controlpointend", 1 },
                   { "sizereductionamount", 2.0 } };
        auto init = WPParticleParser::genParticleInitOp(j, cpSpanInline(cps));
        Particle p = makeParticle(); p.size = 10.0f;
        init(p, 0.0); // idx=0
        init(p, 0.0); // idx=1: scale = 1 - 2 = -1, clamped to 0 → size=0
        CHECK(p.size == doctest::Approx(0.0f));
    }

    TEST_CASE("zero sizereduction leaves size untouched") {
        std::vector<ParticleControlpoint> cps(8, ParticleControlpoint {});
        for (auto& cp : cps) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        cps[1].resolved = Eigen::Vector3d(100, 0, 0);
        json j = { { "name", "mapsequencebetweencontrolpoints" },
                   { "count", 2 },
                   { "controlpointstart", 0 }, { "controlpointend", 1 },
                   { "sizereductionamount", 0.0 } };
        auto init = WPParticleParser::genParticleInitOp(j, cpSpanInline(cps));
        Particle p = makeParticle(); p.size = 99.0f;
        init(p, 0.0);
        init(p, 0.0);
        CHECK(p.size == doctest::Approx(99.0f));
    }

    TEST_CASE("arcamount applies parabolic bulge with default screen-perp arc_dir") {
        std::vector<ParticleControlpoint> cps(8, ParticleControlpoint {});
        for (auto& cp : cps) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        cps[1].resolved = Eigen::Vector3d(100, 0, 0);
        json j = { { "name", "mapsequencebetweencontrolpoints" },
                   { "count", 3 },
                   { "controlpointstart", 0 }, { "controlpointend", 1 },
                   { "arcamount", 0.5 }, { "sizereductionamount", 0.0 } };
        auto init = WPParticleParser::genParticleInitOp(j, cpSpanInline(cps));
        Particle p0 = makeParticle(); init(p0, 0.0);
        Particle p1 = makeParticle(); init(p1, 0.0); // t=0.5
        Particle p2 = makeParticle(); init(p2, 0.0);
        // arc bulge at t=0.5 along default screen-perp = +y.  Magnitude ≈ 50.
        CHECK(std::abs(p1.position.y()) > 30.0f);
        CHECK(std::abs(p1.position.y()) < 80.0f);
    }

    TEST_CASE("explicit arcdirection 0 0 1 routes bulge along z") {
        std::vector<ParticleControlpoint> cps(8, ParticleControlpoint {});
        for (auto& cp : cps) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        cps[1].resolved = Eigen::Vector3d(100, 0, 0);
        json j = { { "name", "mapsequencebetweencontrolpoints" },
                   { "count", 3 },
                   { "controlpointstart", 0 }, { "controlpointend", 1 },
                   { "arcamount", 0.5 }, { "arcdirection", "0 0 1" },
                   { "sizereductionamount", 0.0 } };
        auto init = WPParticleParser::genParticleInitOp(j, cpSpanInline(cps));
        Particle p0 = makeParticle(); init(p0, 0.0);
        Particle p1 = makeParticle(); init(p1, 0.0); // t=0.5 → max bulge along z
        Particle p2 = makeParticle(); init(p2, 0.0);
        CHECK(std::abs(p1.position.z()) > 30.0f);
    }

    TEST_CASE("zero arcamount keeps particle near the line (within noise tolerance)") {
        std::vector<ParticleControlpoint> cps(8, ParticleControlpoint {});
        for (auto& cp : cps) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        cps[1].resolved = Eigen::Vector3d(100, 0, 0);
        json j = { { "name", "mapsequencebetweencontrolpoints" },
                   { "count", 5 },
                   { "controlpointstart", 0 }, { "controlpointend", 1 },
                   { "arcamount", 0.0 }, { "sizereductionamount", 0.0 } };
        auto init = WPParticleParser::genParticleInitOp(j, cpSpanInline(cps));
        Particle p = makeParticle();
        init(p, 0.0); init(p, 0.0); // t=0.25
        CHECK(std::abs(p.position.y()) < 16.0f);
    }

    TEST_CASE("identical CPs (lineLen=0): finite output, no crash") {
        std::vector<ParticleControlpoint> cps(8, ParticleControlpoint {});
        for (auto& cp : cps) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        cps[0].resolved = Eigen::Vector3d(50, 50, 0);
        cps[1].resolved = Eigen::Vector3d(50, 50, 0);
        json j = { { "name", "mapsequencebetweencontrolpoints" },
                   { "count", 3 },
                   { "controlpointstart", 0 }, { "controlpointend", 1 },
                   { "arcamount", 0.5 }, { "sizereductionamount", 0.0 } };
        auto init = WPParticleParser::genParticleInitOp(j, cpSpanInline(cps));
        Particle p = makeParticle();
        init(p, 0.0); init(p, 0.0);
        CHECK(std::isfinite(p.position.x()));
        CHECK(std::isfinite(p.position.y()));
    }

    TEST_CASE("count=1 short-circuits: no positioning, just seq bump") {
        std::vector<ParticleControlpoint> cps(8, ParticleControlpoint {});
        for (auto& cp : cps) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        cps[1].resolved = Eigen::Vector3d(100, 0, 0);
        json j = { { "name", "mapsequencebetweencontrolpoints" },
                   { "count", 1 },
                   { "controlpointstart", 0 }, { "controlpointend", 1 } };
        auto init = WPParticleParser::genParticleInitOp(j, cpSpanInline(cps));
        Particle p = makeParticle();
        init(p, 0.0);
        // count <= 1 short-circuits; particle position untouched (all zero).
        CHECK(p.position.norm() == doctest::Approx(0.0f));
    }

    TEST_CASE("cpStart out of range: short-circuits") {
        std::vector<ParticleControlpoint> cps(2, ParticleControlpoint {});
        for (auto& cp : cps) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        json j = { { "name", "mapsequencebetweencontrolpoints" },
                   { "count", 4 },
                   { "controlpointstart", 5 }, // out of 2-CP range
                   { "controlpointend", 1 } };
        auto init = WPParticleParser::genParticleInitOp(j, cpSpanInline(cps));
        Particle p = makeParticle();
        init(p, 0.0);
        CHECK(p.position.norm() == doctest::Approx(0.0f));
    }

    TEST_CASE("vertical CP line: arc default screen-perp is along x not y") {
        // Line along y → screen-perp = (-line.y, line.x, 0) = (-100, 0, 0).
        // After normalize → (-1, 0, 0).  Bulge is along -x at t=0.5.
        std::vector<ParticleControlpoint> cps(8, ParticleControlpoint {});
        for (auto& cp : cps) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        cps[1].resolved = Eigen::Vector3d(0, 100, 0);
        json j = { { "name", "mapsequencebetweencontrolpoints" },
                   { "count", 3 },
                   { "controlpointstart", 0 }, { "controlpointend", 1 },
                   { "arcamount", 0.5 }, { "sizereductionamount", 0.0 } };
        auto init = WPParticleParser::genParticleInitOp(j, cpSpanInline(cps));
        Particle p0 = makeParticle(); init(p0, 0.0);
        Particle p1 = makeParticle(); init(p1, 0.0); // t=0.5 bulge along -x
        Particle p2 = makeParticle(); init(p2, 0.0);
        // The bulge at t=0.5 should be ~50 along x axis (sign depends on perpendicular).
        CHECK(std::abs(p1.position.x()) > 30.0f);
    }
}

// ============================================================================
// genParticleEmittOp — periodic emission lifecycle (~31 mutants)
// ============================================================================

TEST_SUITE("genParticleEmittOp periodic deep") {
    auto make_emitter = [](float minD, float maxD, float minDur, float maxDur, u32 maxPer) {
        wpscene::Emitter e;
        e.name        = "boxrandom";
        e.rate        = 10.0f;
        e.directions  = { 1, 1, 1 };
        e.distancemin = { 0, 0, 0 };
        e.distancemax = { 0, 0, 0 };
        e.origin      = { 0, 0, 0 };
        e.minperiodicdelay     = minD;
        e.maxperiodicdelay     = maxD;
        e.minperiodicduration  = minDur;
        e.maxperiodicduration  = maxDur;
        e.maxtoemitperperiod   = maxPer;
        e.duration             = 0.0f;
        return e;
    };

    TEST_CASE("no periodic config → returns plain emit op") {
        auto e = make_emitter(0, 0, 0, 0, 0);
        auto op = WPParticleParser::genParticleEmittOp(e, false, 1, 0.0f);
        std::vector<Particle> ps;
        std::vector<ParticleInitOp> inis;
        op(ps, inis, 100, 1.0); // 1 sec at rate=10 → ~10 particles
        CHECK(ps.size() > 0);
    }

    TEST_CASE("duration > 0: outer wrapper stops emission after elapsed > duration") {
        auto e = make_emitter(0, 0, 0, 0, 0);
        e.duration = 0.5f;
        e.rate     = 100.0f;
        auto op = WPParticleParser::genParticleEmittOp(e, false, 1, 0.0f);
        std::vector<Particle> ps;
        std::vector<ParticleInitOp> inis;
        op(ps, inis, 100, 0.4); // elapsed=0.4 ≤ 0.5 → emits
        size_t emit_count_first = ps.size();
        CHECK(emit_count_first > 0);
        op(ps, inis, 100, 0.5); // elapsed=0.9 > 0.5 → no further emits
        size_t emit_count_second = ps.size();
        CHECK(emit_count_second == emit_count_first);
    }

    TEST_CASE("duration=0 disables outer wrapper (continues emitting indefinitely)") {
        auto e = make_emitter(0, 0, 0, 0, 0);
        e.duration = 0.0f;
        e.rate     = 100.0f;
        auto op = WPParticleParser::genParticleEmittOp(e, false, 1, 0.0f);
        std::vector<Particle> ps;
        std::vector<ParticleInitOp> inis;
        op(ps, inis, 1000, 1.0);
        size_t first = ps.size();
        op(ps, inis, 1000, 1.0);
        CHECK(ps.size() > first);
    }

    TEST_CASE("unknown emitter name → no-op op") {
        wpscene::Emitter e;
        e.name = "unknownEmitterShape";
        auto op = WPParticleParser::genParticleEmittOp(e, false, 1, 0.0f);
        std::vector<Particle> ps;
        std::vector<ParticleInitOp> inis;
        op(ps, inis, 100, 1.0);
        CHECK(ps.empty());
    }

    TEST_CASE("sphere emitter (sphererandom) wires up correctly") {
        wpscene::Emitter e;
        e.name        = "sphererandom";
        e.rate        = 5.0f;
        e.directions  = { 1, 1, 1 };
        e.distancemin = { 0, 0, 0 };
        e.distancemax = { 100, 0, 0 };
        e.origin      = { 0, 0, 0 };
        auto op = WPParticleParser::genParticleEmittOp(e, false, 1, 0.0f);
        std::vector<Particle> ps;
        std::vector<ParticleInitOp> inis;
        op(ps, inis, 100, 1.0);
        CHECK(ps.size() > 0);
    }

    TEST_CASE("periodic with active duration: emits during active phases only") {
        // Short active (0.1) + long inactive (0.9) → 10% duty cycle.  Verify just that
        // we don't crash and produce some particles.
        auto e = make_emitter(0.9f, 0.9f, 0.1f, 0.1f, 0);
        e.rate = 100.0f;
        auto op = WPParticleParser::genParticleEmittOp(e, false, 1, 0.0f);
        std::vector<Particle> ps;
        std::vector<ParticleInitOp> inis;
        for (int i = 0; i < 10; i++) op(ps, inis, 1000, 0.1);
        CHECK(true);
    }

    TEST_CASE("periodic with maxtoemitperperiod limits emission count") {
        auto e = make_emitter(0.5f, 0.5f, 0.5f, 0.5f, 3);
        e.rate = 1000.0f;
        auto op = WPParticleParser::genParticleEmittOp(e, false, 1, 0.0f);
        std::vector<Particle> ps;
        std::vector<ParticleInitOp> inis;
        op(ps, inis, 100, 0.1);
        for (int i = 0; i < 5; i++) op(ps, inis, 100, 0.05);
        CHECK(ps.size() <= 100);
    }

    TEST_CASE("duration boundary exact: elapsed == duration still emits (<=)") {
        auto e = make_emitter(0, 0, 0, 0, 0);
        e.duration = 0.1f;
        e.rate     = 100.0f;
        auto op = WPParticleParser::genParticleEmittOp(e, false, 1, 0.0f);
        std::vector<Particle> ps;
        std::vector<ParticleInitOp> inis;
        op(ps, inis, 100, 0.1); // elapsed = 0.1 == duration → still emits (<=)
        size_t after_first = ps.size();
        CHECK(after_first > 0);
        op(ps, inis, 100, 0.001); // elapsed = 0.101 > duration → no emit
        size_t after_second = ps.size();
        CHECK(after_second == after_first);
    }

    TEST_CASE("hasPeriodic depends on maxperiodicduration > 0 OR maxperiodicdelay > 0") {
        // Just maxperiodicduration set → wrapper applies.  Test by running for
        // long enough that the duration short window will end and silence the
        // emitter for several frames.
        auto e = make_emitter(0, 0, 0, 0.5f, 0); // duration=0.5
        e.rate = 100.0f;
        auto op = WPParticleParser::genParticleEmittOp(e, false, 1, 0.0f);
        std::vector<Particle> ps;
        std::vector<ParticleInitOp> inis;
        // Run 20 frames at 0.1s each = 2s total — multiple phase cycles.
        // Just ensure no crash and finite output.
        for (int i = 0; i < 20; i++) op(ps, inis, 1000, 0.1);
        CHECK(true);
    }

    TEST_CASE("active flag flips on each phaseDur boundary") {
        // Set very short phases so we cross many boundaries quickly.
        // 10 calls × 0.1s = 1s total ≫ 0.05s phase → many flips.
        auto e = make_emitter(0.05f, 0.05f, 0.05f, 0.05f, 0);
        e.rate = 100.0f;
        auto op = WPParticleParser::genParticleEmittOp(e, false, 1, 0.0f);
        std::vector<Particle> ps;
        std::vector<ParticleInitOp> inis;
        for (int i = 0; i < 10; i++) op(ps, inis, 10000, 0.1);
        // Just verify no crash; emission count is non-deterministic.
        CHECK(true);
    }
}

// ============================================================================
// vortex_v2 operator — surviving 18 mutants
// ============================================================================

TEST_SUITE("vortex_v2 deep") {
    TEST_CASE("acceleration mode: inner zone applies speedinner") {
        json j  = { { "name", "vortex_v2" },
                    { "controlpoint", 0 }, { "axis", "0 0 1" },
                    { "speedinner", 100.0 }, { "speedouter", 0.0 },
                    { "distanceinner", 100.0 }, { "distanceouter", 200.0 },
                    { "flags", 0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(50, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(std::abs(out.velocity.y()) > 0.0f);
    }

    TEST_CASE("acceleration mode: middle zone produces non-zero tangential velocity") {
        json j  = { { "name", "vortex_v2" },
                    { "controlpoint", 0 }, { "axis", "0 0 1" },
                    { "speedinner", 0.0 }, { "speedouter", 100.0 },
                    { "distanceinner", 50.0 }, { "distanceouter", 150.0 },
                    { "flags", 0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(100, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(std::abs(out.velocity.y()) > 0.0f);
    }

    TEST_CASE("acceleration mode: outer zone applies speedouter") {
        json j  = { { "name", "vortex_v2" },
                    { "controlpoint", 0 }, { "axis", "0 0 1" },
                    { "speedinner", 0.0 }, { "speedouter", 100.0 },
                    { "distanceinner", 50.0 }, { "distanceouter", 100.0 },
                    { "flags", 0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(200, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(std::abs(out.velocity.y()) > 0.0f);
    }

    TEST_CASE("rotation mode (flags=2): orbits around axis preserving radius") {
        json j  = { { "name", "vortex_v2" },
                    { "controlpoint", 0 }, { "axis", "0 0 1" },
                    { "speedinner", 1.0 }, { "speedouter", 1.0 },
                    { "distanceinner", 50.0 }, { "distanceouter", 100.0 },
                    { "flags", 2 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(100, 0, 0);
        auto out = runOpSingle(op, p, 0.1);
        double radius = std::sqrt((double)out.position.x() * out.position.x() +
                                  (double)out.position.y() * out.position.y());
        CHECK(radius == doctest::Approx(100.0).epsilon(0.05));
        CHECK(out.velocity.norm() == doctest::Approx(0.0f).epsilon(1e-3));
    }

    TEST_CASE("rotation mode skipped at distance < 0.001 (no NaN)") {
        json j  = { { "name", "vortex_v2" },
                    { "controlpoint", 0 }, { "axis", "0 0 1" },
                    { "speedinner", 1.0 }, { "speedouter", 1.0 },
                    { "distanceinner", 1.0 }, { "distanceouter", 5.0 },
                    { "flags", 2 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        auto out = runOpSingle(op, p);
        CHECK(std::isfinite(out.position.x()));
        CHECK(std::isfinite(out.position.y()));
    }

    TEST_CASE("ringradius pull pushes outside particle inward") {
        json j  = { { "name", "vortex_v2" },
                    { "controlpoint", 0 }, { "axis", "0 0 1" },
                    { "speedinner", 0.0 }, { "speedouter", 0.0 },
                    { "distanceinner", 10.0 }, { "distanceouter", 50.0 },
                    { "flags", 0 },
                    { "ringradius", 100.0 }, { "ringwidth", 5.0 },
                    { "ringpulldistance", 10.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(150, 0, 0);
        auto out = runOpSingle(op, p, 0.1);
        CHECK(out.velocity.x() < 0.0f);
    }

    TEST_CASE("ringradius pull pushes inside particle outward") {
        json j  = { { "name", "vortex_v2" },
                    { "controlpoint", 0 }, { "axis", "0 0 1" },
                    { "speedinner", 0.0 }, { "speedouter", 0.0 },
                    { "distanceinner", 10.0 }, { "distanceouter", 50.0 },
                    { "flags", 0 },
                    { "ringradius", 100.0 }, { "ringwidth", 5.0 },
                    { "ringpulldistance", 10.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(50, 0, 0);
        auto out = runOpSingle(op, p, 0.1);
        CHECK(out.velocity.x() > 0.0f);
    }

    TEST_CASE("ringradius within ringwidth: no pull applied") {
        json j  = { { "name", "vortex_v2" },
                    { "controlpoint", 0 }, { "axis", "0 0 1" },
                    { "speedinner", 0.0 }, { "speedouter", 0.0 },
                    { "distanceinner", 10.0 }, { "distanceouter", 50.0 },
                    { "flags", 0 },
                    { "ringradius", 100.0 }, { "ringwidth", 50.0 },
                    { "ringpulldistance", 1000.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(95, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(std::abs(out.velocity.x()) < 1e-3f);
    }

    TEST_CASE("dis_mid<0 collapses to all-inner branch in vortex_v2") {
        json j  = { { "name", "vortex_v2" },
                    { "controlpoint", 0 }, { "axis", "0 0 1" },
                    { "speedinner", 100.0 }, { "speedouter", 0.0 },
                    { "distanceinner", 100.0 }, { "distanceouter", 90.0 },
                    { "flags", 0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(95, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(std::abs(out.velocity.y()) > 0.0f);
    }

    TEST_CASE("dead particle (LifetimeOk false) skipped in vortex_v2") {
        json j  = { { "name", "vortex_v2" },
                    { "controlpoint", 0 }, { "axis", "0 0 1" },
                    { "speedinner", 100.0 }, { "speedouter", 0.0 },
                    { "distanceinner", 100.0 }, { "distanceouter", 200.0 },
                    { "flags", 0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.lifetime = 0.0f;
        p.position = Eigen::Vector3f(50, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.velocity.x() == doctest::Approx(0.0f));
        CHECK(out.velocity.y() == doctest::Approx(0.0f));
    }

    TEST_CASE("rotation mode: x-displacement scales with angle (smaller radius → larger angle)") {
        // Two particles at different radii, same speed → smaller-radius one
        // sweeps a larger angular velocity (angle = speed/distance * dt).
        // Both move tangentially (y) by ~speed*dt, but x-displacement scales
        // with the cosine miss: delta_x = r * (cos(angle) - 1) ≈ -r*angle²/2
        // Inner (r=50, angle=0.1) → -0.25.  Outer (r=500, angle=0.01) → -0.025.
        json j  = { { "name", "vortex_v2" },
                    { "controlpoint", 0 }, { "axis", "0 0 1" },
                    { "speedinner", 50.0 }, { "speedouter", 50.0 },
                    { "distanceinner", 1.0 }, { "distanceouter", 1000.0 },
                    { "flags", 2 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p_inner = makeParticle();
        p_inner.position = Eigen::Vector3f(50, 0, 0);
        auto p_outer = makeParticle();
        p_outer.position = Eigen::Vector3f(500, 0, 0);
        auto out_inner = runOpSingle(op, p_inner, 0.1);
        auto out_outer = runOpSingle(op, p_outer, 0.1);
        const float dx_inner = std::abs(out_inner.position.x() - 50.0f);
        const float dx_outer = std::abs(out_outer.position.x() - 500.0f);
        // Inner has |dx| ~ 0.25, outer has ~ 0.025 → inner moves further in x relative.
        CHECK(dx_inner > dx_outer);
    }

    TEST_CASE("middle-zone speed lerps linearly between inner and outer (acceleration mode)") {
        // Pin precise math: speedinner=10, speedouter=110, dis_mid=100.
        // At dist=50 (mid), t=0 → speed=10.  At dist=150 (full), t=1 → speed=110.
        // We can't test absolute velocity numbers (direction normalised) but
        // can test relative magnitudes between two test cases.
        json base = { { "name", "vortex_v2" },
                      { "controlpoint", 0 }, { "axis", "0 0 1" },
                      { "speedinner", 10.0 }, { "speedouter", 110.0 },
                      { "distanceinner", 50.0 }, { "distanceouter", 150.0 },
                      { "flags", 0 } };
        auto op = WPParticleParser::genParticleOperatorOp(base, emptyOverride());
        auto p_close = makeParticle();
        p_close.position = Eigen::Vector3f(60, 0, 0); // very close to inner edge
        auto p_far = makeParticle();
        p_far.position = Eigen::Vector3f(140, 0, 0); // very close to outer edge
        auto out_close = runOpSingle(op, p_close, 0.1);
        auto out_far = runOpSingle(op, p_far, 0.1);
        // Far particle gets speedouter=110 (high); close gets near speedinner=10.
        // Velocity magnitude: far > close.
        CHECK(out_far.velocity.norm() > out_close.velocity.norm() * 2.0f);
    }
}

// ============================================================================
// colorlist initializer — surviving 14 mutants
// ============================================================================

TEST_SUITE("colorlist deep") {
    TEST_CASE("string array color in 0..1 space passes through unscaled") {
        json j = { { "name", "colorlist" }, { "colors", { "1.0 0.5 0.0" } } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle(); init(p, 0.0);
        CHECK(p.color.x() == doctest::Approx(1.0f));
        CHECK(p.color.y() == doctest::Approx(0.5f));
        CHECK(p.color.z() == doctest::Approx(0.0f));
    }

    TEST_CASE("string array color in 0..255 space gets scaled") {
        json j = { { "name", "colorlist" }, { "colors", { "255 128 0" } } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle(); init(p, 0.0);
        CHECK(p.color.x() == doctest::Approx(1.0f).epsilon(0.001));
        CHECK(p.color.y() == doctest::Approx(128.0f / 255.0f).epsilon(0.001));
        CHECK(p.color.z() == doctest::Approx(0.0f));
    }

    TEST_CASE("array form color [r,g,b]") {
        json j = { { "name", "colorlist" },
                   { "colors", json::array({ json::array({ 0.2, 0.4, 0.6 }) }) } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle(); init(p, 0.0);
        CHECK(p.color.x() == doctest::Approx(0.2f).epsilon(0.001));
        CHECK(p.color.y() == doctest::Approx(0.4f).epsilon(0.001));
        CHECK(p.color.z() == doctest::Approx(0.6f).epsilon(0.001));
    }

    TEST_CASE("array form color in 0..255 also gets scaled") {
        json j = { { "name", "colorlist" },
                   { "colors", json::array({ json::array({ 200.0, 100.0, 50.0 }) }) } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle(); init(p, 0.0);
        CHECK(p.color.x() == doctest::Approx(200.0f / 255.0f).epsilon(0.001));
        CHECK(p.color.y() == doctest::Approx(100.0f / 255.0f).epsilon(0.001));
        CHECK(p.color.z() == doctest::Approx(50.0f / 255.0f).epsilon(0.001));
    }

    TEST_CASE("empty colors → palette gets default white") {
        json j = { { "name", "colorlist" }, { "colors", json::array() } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle(); init(p, 0.0);
        CHECK(p.color.x() == doctest::Approx(1.0f));
        CHECK(p.color.y() == doctest::Approx(1.0f));
        CHECK(p.color.z() == doctest::Approx(1.0f));
    }

    TEST_CASE("missing colors key → palette gets default white") {
        json j = { { "name", "colorlist" } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle(); init(p, 0.0);
        CHECK(p.color.x() == doctest::Approx(1.0f));
    }

    TEST_CASE("no jitter (default) returns base color verbatim") {
        json j = { { "name", "colorlist" }, { "colors", { "0.7 0.2 0.5" } } };
        auto init = WPParticleParser::genParticleInitOp(j);
        for (int i = 0; i < 5; i++) {
            Particle p = makeParticle(); init(p, 0.0);
            CHECK(p.color.x() == doctest::Approx(0.7f));
            CHECK(p.color.y() == doctest::Approx(0.2f));
            CHECK(p.color.z() == doctest::Approx(0.5f));
        }
    }

    TEST_CASE("with valuenoise, color drifts (no longer constant)") {
        json j = { { "name", "colorlist" },
                   { "colors", { "0.5 0.5 0.5" } },
                   { "valuenoise", 0.3 } };
        auto init = WPParticleParser::genParticleInitOp(j);
        bool any_drift = false;
        for (int i = 0; i < 30; i++) {
            Particle p = makeParticle(); init(p, 0.0);
            if (std::abs(p.color.x() - 0.5f) > 0.001f ||
                std::abs(p.color.y() - 0.5f) > 0.001f ||
                std::abs(p.color.z() - 0.5f) > 0.001f) {
                any_drift = true;
                break;
            }
        }
        CHECK(any_drift);
    }

    TEST_CASE("comma-separated color string parses") {
        json j = { { "name", "colorlist" }, { "colors", { "1.0,0.5,0.0" } } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle(); init(p, 0.0);
        CHECK(p.color.x() == doctest::Approx(1.0f));
        CHECK(p.color.y() == doctest::Approx(0.5f));
        CHECK(p.color.z() == doctest::Approx(0.0f));
    }

    TEST_CASE("threshold exactly at 1.5: <= 1.5 not scaled") {
        json j = { { "name", "colorlist" }, { "colors", { "1.5 1.0 1.0" } } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle(); init(p, 0.0);
        CHECK(p.color.x() == doctest::Approx(1.5f));
    }

    TEST_CASE("any one component > 1.5 triggers full normalization (RGB)") {
        json j = { { "name", "colorlist" }, { "colors", { "10 0 0" } } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle(); init(p, 0.0);
        CHECK(p.color.x() == doctest::Approx(10.0f / 255.0f).epsilon(0.001));
        CHECK(p.color.y() == doctest::Approx(0.0f));
        CHECK(p.color.z() == doctest::Approx(0.0f));
    }

    TEST_CASE("multiple palette entries → colors picked randomly across calls") {
        json j = { { "name", "colorlist" },
                   { "colors", { "1.0 0.0 0.0", "0.0 0.0 1.0" } } };
        auto init = WPParticleParser::genParticleInitOp(j);
        bool saw_red = false, saw_blue = false;
        for (int i = 0; i < 100; i++) {
            Particle p = makeParticle(); init(p, 0.0);
            if (p.color.x() > 0.9f) saw_red = true;
            if (p.color.z() > 0.9f) saw_blue = true;
            if (saw_red && saw_blue) break;
        }
        CHECK(saw_red);
        CHECK(saw_blue);
    }
}

// ============================================================================
// boids operator — surviving 10 mutants.  Multi-particle tests use a single
// pre-allocated vector and INDEX access.
// ============================================================================

TEST_SUITE("boids deep") {
    TEST_CASE("single particle (no neighbours): only maxspeed clamp can apply") {
        json j  = { { "name", "boids" },
                    { "neighborthreshold", 100.0 },
                    { "separationthreshold", 50.0 },
                    { "separationfactor", 1.0 },
                    { "cohesionfactor", 1.0 },
                    { "alignmentfactor", 1.0 },
                    { "maxspeed", 1000.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.velocity = Eigen::Vector3f(10, 0, 0);
        auto out = runOpSingle(op, p);
        // No neighbours → no force.  vel still 10 (under maxspeed=1000).
        CHECK(out.velocity.x() == doctest::Approx(10.0f).epsilon(1e-3));
    }

    TEST_CASE("two close particles: separation pushes them apart") {
        json j  = { { "name", "boids" },
                    { "neighborthreshold", 100.0 },
                    { "separationthreshold", 50.0 },
                    { "separationfactor", 100.0 },
                    { "cohesionfactor", 0.0 },
                    { "alignmentfactor", 0.0 },
                    { "maxspeed", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        std::vector<Particle> ps; ps.reserve(8);
        ps.push_back(makeParticle());
        ps.push_back(makeParticle());
        ps[0].position  = Eigen::Vector3f(0, 0, 0);
        ps[1].position  = Eigen::Vector3f(10, 0, 0);
        std::vector<ParticleControlpoint> cps;
        auto cp_span_ = defaultCps(cps);
        ParticleInfo info {
            .particles     = std::span<Particle>(ps),
            .controlpoints = cp_span_,
            .time          = 0.0,
            .time_pass     = 0.1,
        };
        op(info);
        CHECK(ps[0].velocity.x() < 0.0f);
        CHECK(ps[1].velocity.x() > 0.0f);
    }

    TEST_CASE("alignment matches average neighbour velocity") {
        json j  = { { "name", "boids" },
                    { "neighborthreshold", 10000.0 },
                    { "separationthreshold", 0.001 },
                    { "separationfactor", 0.0 },
                    { "cohesionfactor", 0.0 },
                    { "alignmentfactor", 100.0 },
                    { "maxspeed", 10000.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        std::vector<Particle> ps; ps.reserve(8);
        ps.push_back(makeParticle());
        ps.push_back(makeParticle());
        ps[0].position = Eigen::Vector3f(0, 0, 0);
        ps[1].position = Eigen::Vector3f(50, 0, 0);
        ps[1].velocity = Eigen::Vector3f(100, 0, 0);
        std::vector<ParticleControlpoint> cps;
        auto cp_span_ = defaultCps(cps);
        ParticleInfo info {
            .particles     = std::span<Particle>(ps),
            .controlpoints = cp_span_,
            .time          = 0.0,
            .time_pass     = 0.1,
        };
        op(info);
        CHECK(ps[0].velocity.x() > 0.0f);
    }

    TEST_CASE("cohesion pulls toward neighbour centroid") {
        json j  = { { "name", "boids" },
                    { "neighborthreshold", 10000.0 },
                    { "separationthreshold", 0.001 },
                    { "separationfactor", 0.0 },
                    { "cohesionfactor", 100.0 },
                    { "alignmentfactor", 0.0 },
                    { "maxspeed", 10000.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        std::vector<Particle> ps; ps.reserve(8);
        ps.push_back(makeParticle());
        ps.push_back(makeParticle());
        ps[0].position = Eigen::Vector3f(0, 0, 0);
        ps[1].position = Eigen::Vector3f(100, 0, 0);
        std::vector<ParticleControlpoint> cps;
        auto cp_span_ = defaultCps(cps);
        ParticleInfo info {
            .particles     = std::span<Particle>(ps),
            .controlpoints = cp_span_,
            .time          = 0.0,
            .time_pass     = 0.1,
        };
        op(info);
        CHECK(ps[0].velocity.x() > 0.0f);
    }

    TEST_CASE("maxspeed clamp activates when speed > maxspeed") {
        json j  = { { "name", "boids" },
                    { "neighborthreshold", 0.001 },
                    { "separationthreshold", 0.001 },
                    { "separationfactor", 0.0 },
                    { "cohesionfactor", 0.0 },
                    { "alignmentfactor", 0.0 },
                    { "maxspeed", 50.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.velocity = Eigen::Vector3f(200, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.velocity.norm() == doctest::Approx(50.0f).epsilon(0.05));
    }

    TEST_CASE("maxspeed clamp NOT applied when speed already <= maxspeed") {
        json j  = { { "name", "boids" },
                    { "neighborthreshold", 0.001 },
                    { "separationthreshold", 0.001 },
                    { "separationfactor", 0.0 },
                    { "cohesionfactor", 0.0 },
                    { "alignmentfactor", 0.0 },
                    { "maxspeed", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.velocity = Eigen::Vector3f(40, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.velocity.norm() == doctest::Approx(40.0f));
    }
}

// ============================================================================
// hsvcolorrandom (lines 458-490) — surviving 9 mutants
// ============================================================================

TEST_SUITE("hsvcolorrandom deep") {
    TEST_CASE("hue range with positive span keeps non-wrap path") {
        json j = { { "name", "hsvcolorrandom" },
                   { "huemin", 0.0 }, { "huemax", 360.0 },
                   { "saturationmin", 1.0 }, { "saturationmax", 1.0 },
                   { "valuemin", 1.0 }, { "valuemax", 1.0 } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle(); init(p, 0.0);
        CHECK(p.color.x() >= 0.0f);
        CHECK(p.color.x() <= 1.0f);
    }

    TEST_CASE("hue range wraps when huemax < huemin") {
        json j = { { "name", "hsvcolorrandom" },
                   { "huemin", 350.0 }, { "huemax", 10.0 },
                   { "saturationmin", 1.0 }, { "saturationmax", 1.0 },
                   { "valuemin", 1.0 }, { "valuemax", 1.0 } };
        auto init = WPParticleParser::genParticleInitOp(j);
        for (int i = 0; i < 10; i++) {
            Particle p = makeParticle(); init(p, 0.0);
            CHECK(p.color.x() >= 0.0f);
            CHECK(p.color.x() <= 1.0f);
        }
    }

    TEST_CASE("huesteps >= 1 produces quantised hue") {
        json j = { { "name", "hsvcolorrandom" },
                   { "huemin", 0.0 }, { "huemax", 360.0 },
                   { "saturationmin", 1.0 }, { "saturationmax", 1.0 },
                   { "valuemin", 1.0 }, { "valuemax", 1.0 },
                   { "huesteps", 2 } };
        auto init = WPParticleParser::genParticleInitOp(j);
        for (int i = 0; i < 20; i++) {
            Particle p = makeParticle(); init(p, 0.0);
            CHECK(p.color.x() >= 0.0f);
            CHECK(p.color.x() <= 1.0f);
        }
    }

    TEST_CASE("huesteps == 0 → continuous hue (h_quant = 0 path)") {
        json j = { { "name", "hsvcolorrandom" },
                   { "huemin", 0.0 }, { "huemax", 360.0 },
                   { "saturationmin", 1.0 }, { "saturationmax", 1.0 },
                   { "valuemin", 1.0 }, { "valuemax", 1.0 },
                   { "huesteps", 0 } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle(); init(p, 0.0);
        CHECK(p.color.x() >= 0.0f);
    }

    TEST_CASE("saturation max < min clamped to 0 by max(0,…)") {
        json j = { { "name", "hsvcolorrandom" },
                   { "huemin", 0.0 }, { "huemax", 0.0 },
                   { "saturationmin", 1.0 }, { "saturationmax", 0.5 },
                   { "valuemin", 1.0 }, { "valuemax", 1.0 } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle(); init(p, 0.0);
        // satmax-satmin = -0.5 → max(0)=0 → s = satmin = 1.  hue=0, s=1, v=1 → red.
        CHECK(p.color.x() == doctest::Approx(1.0f).epsilon(0.05));
        CHECK(p.color.y() == doctest::Approx(0.0f).epsilon(0.05));
        CHECK(p.color.z() == doctest::Approx(0.0f).epsilon(0.05));
    }

    TEST_CASE("value max < min clamped to 0 by max(0,…)") {
        json j = { { "name", "hsvcolorrandom" },
                   { "huemin", 0.0 }, { "huemax", 0.0 },
                   { "saturationmin", 1.0 }, { "saturationmax", 1.0 },
                   { "valuemin", 1.0 }, { "valuemax", 0.3 } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle(); init(p, 0.0);
        CHECK(p.color.x() == doctest::Approx(1.0f).epsilon(0.05));
    }
}

// ============================================================================
// genOverrideInitOp — brightness branch survivors
// ============================================================================

TEST_SUITE("genOverrideInitOp deep") {
    TEST_CASE("brightness > 1.0 reduces alpha by 1/sqrt(brightness)") {
        wpscene::ParticleInstanceoverride o;
        o.enabled    = true;
        o.brightness = 4.0f;
        auto init = WPParticleParser::genOverrideInitOp(o);
        Particle p = makeParticle();
        init(p, 0.0);
        CHECK(p.alpha == doctest::Approx(0.5f).epsilon(0.01));
        CHECK(p.init.color.x() == doctest::Approx(4.0f).epsilon(0.01));
    }

    TEST_CASE("brightness < 1.0 multiplies color but does NOT divide alpha") {
        wpscene::ParticleInstanceoverride o;
        o.enabled    = true;
        o.brightness = 0.5f;
        auto init = WPParticleParser::genOverrideInitOp(o);
        Particle p = makeParticle();
        init(p, 0.0);
        CHECK(p.alpha == doctest::Approx(1.0f).epsilon(0.01));
        CHECK(p.init.color.x() == doctest::Approx(0.5f).epsilon(0.01));
    }

    TEST_CASE("brightness == 1.0 short-circuits") {
        wpscene::ParticleInstanceoverride o;
        o.enabled    = true;
        o.brightness = 1.0f;
        auto init = WPParticleParser::genOverrideInitOp(o);
        Particle p = makeParticle();
        init(p, 0.0);
        CHECK(p.init.color.x() == doctest::Approx(1.0f).epsilon(0.01));
        CHECK(p.alpha == doctest::Approx(1.0f).epsilon(0.01));
    }
}

// ============================================================================
// FadeValueChange / structs (lines 670-770) — surviving 9 mutants
// ============================================================================

TEST_SUITE("FadeValueChange deep") {
    TEST_CASE("life <= start returns startValue") {
        json j = { { "name", "sizechange" },
                   { "starttime", 0.5f }, { "endtime", 1.0f },
                   { "startvalue", 1.0f }, { "endvalue", 5.0f } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.lifetime = 1.0f;
        p.size = 10.0f;
        auto out = runOpSingle(op, p);
        CHECK(out.size == doctest::Approx(10.0f).epsilon(0.01));
    }

    TEST_CASE("life > end returns endValue") {
        json j = { { "name", "sizechange" },
                   { "starttime", 0.0f }, { "endtime", 0.5f },
                   { "startvalue", 1.0f }, { "endvalue", 5.0f } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.lifetime = 0.001f;
        p.size = 10.0f;
        auto out = runOpSingle(op, p);
        CHECK(out.size == doctest::Approx(50.0f).epsilon(0.05));
    }

    TEST_CASE("life within range linearly interpolates") {
        json j = { { "name", "sizechange" },
                   { "starttime", 0.0f }, { "endtime", 1.0f },
                   { "startvalue", 0.0f }, { "endvalue", 10.0f } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.lifetime = 0.5f;
        p.size = 1.0f;
        auto out = runOpSingle(op, p);
        CHECK(out.size == doctest::Approx(5.0f).epsilon(0.05));
    }

    TEST_CASE("non-zero start and end pinpoint pass = (life-start)/(end-start)") {
        // Pin pass formula with start=0.5, end=1.0, life=0.7 → pass=0.4 → out=4.
        // Sub-to-add mutations would yield ≈24 or ≈1.33 (very different).
        json j = { { "name", "sizechange" },
                   { "starttime", 0.5f }, { "endtime", 1.0f },
                   { "startvalue", 0.0f }, { "endvalue", 10.0f } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.lifetime = 0.3f; // life-pos = 1 - 0.3 = 0.7
        p.size = 1.0f;
        auto out = runOpSingle(op, p);
        // pass=0.4 → out = lerp(0.4, 0, 10) = 4.  size *= 4 = 4.
        CHECK(out.size == doctest::Approx(4.0f).epsilon(0.05));
    }

    TEST_CASE("FadeValueChange life exactly at start: returns startValue") {
        // life-pos = starttime exactly → returns startValue (not NaN, not lerp).
        json j = { { "name", "sizechange" },
                   { "starttime", 0.5f }, { "endtime", 1.0f },
                   { "startvalue", 3.0f }, { "endvalue", 7.0f } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.lifetime = 0.5f; // life-pos = 0.5 = start exactly
        p.size = 1.0f;
        auto out = runOpSingle(op, p);
        // At life==start, returns startValue=3 → size *= 3 = 3.
        CHECK(out.size == doctest::Approx(3.0f).epsilon(0.05));
    }

    TEST_CASE("FadeValueChange life beyond end: returns endValue") {
        json j = { { "name", "sizechange" },
                   { "starttime", 0.0f }, { "endtime", 0.5f },
                   { "startvalue", 3.0f }, { "endvalue", 7.0f } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.lifetime = 0.001f; // life-pos ≈ 1 > end=0.5
        p.size = 1.0f;
        auto out = runOpSingle(op, p);
        CHECK(out.size == doctest::Approx(7.0f).epsilon(0.05));
    }
}

// ============================================================================
// alphafade — surviving mutants on fade-in/out window
// ============================================================================

TEST_SUITE("alphafade deep") {
    // makeParticle() defaults: init.lifetime=1, p.lifetime=1, alpha=1.
    // LifetimePos = 1 - p.lifetime/init.lifetime; tests vary p.lifetime to
    // sweep age01.

    TEST_CASE("age01=0 with fadeintime=0.5: full fade-in attenuation") {
        // p.lifetime=1.0 → age01=0; age01 < fadeintime → a = 0/0.5 = 0.
        json j = { { "name", "alphafade" },
                   { "fadeintime", 0.5f }, { "fadeouttime", 0.5f } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.lifetime = 1.0f;
        p.alpha = 1.0f;
        auto out = runOpSingle(op, p);
        CHECK(out.alpha == doctest::Approx(0.0f));
    }

    TEST_CASE("age01 near 1 with fadeouttime=0.5: fade-out ramps to 0") {
        // p.lifetime=0.001 → age01≈0.999; a = (1-0.999)/(1-0.5) ≈ 0.002.
        json j = { { "name", "alphafade" },
                   { "fadeintime", 0.0f }, { "fadeouttime", 0.5f } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.lifetime = 0.001f;
        p.alpha = 1.0f;
        auto out = runOpSingle(op, p);
        CHECK(out.alpha == doctest::Approx(0.0f).epsilon(0.05));
    }

    TEST_CASE("age01=0.5, fadeintime=0, fadeouttime=0: midlife fade-out") {
        // WE semantic foot-gun: fadeouttime=0 means fade-out STARTS at age=0,
        // so alpha ramps 1.0 → 0 across the entire life.  At midlife: 0.5.
        json j = { { "name", "alphafade" },
                   { "fadeintime", 0.0f }, { "fadeouttime", 0.0f } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.lifetime = 0.5f;
        p.alpha = 1.0f;
        auto out = runOpSingle(op, p);
        CHECK(out.alpha == doctest::Approx(0.5f));
    }

    TEST_CASE("init.lifetime <= epsilon → particle skipped") {
        // Defensive guard keeps the operator from dividing by ~0.
        json j = { { "name", "alphafade" },
                   { "fadeintime", 0.5f }, { "fadeouttime", 0.5f } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.lifetime = 1.0f;
        p.init.lifetime = 1e-9f;
        p.alpha = 1.0f;
        auto out = runOpSingle(op, p);
        CHECK(out.alpha == doctest::Approx(1.0f));
    }

    TEST_CASE("age01=0.5 inside plateau (fadeintime=0.2, fadeouttime=0.8): no fade") {
        // 0.2 ≤ age01 ≤ 0.8 → middle plateau → alpha unchanged.
        json j = { { "name", "alphafade" },
                   { "fadeintime", 0.2f }, { "fadeouttime", 0.8f } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.lifetime = 0.5f;
        p.alpha = 1.0f;
        auto out = runOpSingle(op, p);
        CHECK(out.alpha == doctest::Approx(1.0f));
    }
}

// ============================================================================
// reducemovementnearcontrolpoint — surviving 7 mutants
// ============================================================================

TEST_SUITE("reducemovementnearcontrolpoint deep") {
    TEST_CASE("dist <= distanceinner: full reduction") {
        json j  = { { "name", "reducemovementnearcontrolpoint" },
                    { "controlpoint", 0 },
                    { "distanceinner", 50.0 }, { "distanceouter", 100.0 },
                    { "reductioninner", 1.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(10, 0, 0);
        p.velocity = Eigen::Vector3f(100, 0, 0);
        auto out = runOpSingle(op, p, 1.0);
        // factor = 1/(1+1) = 0.5 → 100 * 0.5 = 50.
        CHECK(out.velocity.x() == doctest::Approx(50.0f).epsilon(0.05));
    }

    TEST_CASE("dist >= distanceouter: skip (no reduction)") {
        json j  = { { "name", "reducemovementnearcontrolpoint" },
                    { "controlpoint", 0 },
                    { "distanceinner", 50.0 }, { "distanceouter", 100.0 },
                    { "reductioninner", 1.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(150, 0, 0);
        p.velocity = Eigen::Vector3f(100, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.velocity.x() == doctest::Approx(100.0f));
    }

    TEST_CASE("middle zone lerps reduction toward 0") {
        json j  = { { "name", "reducemovementnearcontrolpoint" },
                    { "controlpoint", 0 },
                    { "distanceinner", 50.0 }, { "distanceouter", 150.0 },
                    { "reductioninner", 1.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(100, 0, 0);
        p.velocity = Eigen::Vector3f(100, 0, 0);
        auto out = runOpSingle(op, p, 1.0);
        // reduction = 0.5; factor = 1/1.5 ≈ 0.667.
        CHECK(out.velocity.x() == doctest::Approx(66.67f).epsilon(0.1));
    }

    TEST_CASE("dead particle skipped") {
        json j  = { { "name", "reducemovementnearcontrolpoint" },
                    { "controlpoint", 0 },
                    { "distanceinner", 50.0 }, { "distanceouter", 100.0 },
                    { "reductioninner", 1.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.lifetime = 0.0f;
        p.position = Eigen::Vector3f(0, 0, 0);
        p.velocity = Eigen::Vector3f(100, 0, 0);
        auto out = runOpSingle(op, p, 1.0);
        CHECK(out.velocity.x() == doctest::Approx(100.0f));
    }

    TEST_CASE("dist exactly at distanceouter: skip (>= boundary)") {
        json j  = { { "name", "reducemovementnearcontrolpoint" },
                    { "controlpoint", 0 },
                    { "distanceinner", 50.0 }, { "distanceouter", 100.0 },
                    { "reductioninner", 1.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(100, 0, 0); // exactly at outer
        p.velocity = Eigen::Vector3f(100, 0, 0);
        auto out = runOpSingle(op, p, 1.0);
        // dist >= distanceouter → continue → no reduction.  velocity unchanged.
        CHECK(out.velocity.x() == doctest::Approx(100.0f));
    }
}

// ============================================================================
// vortex (1070-1095) — surviving 10 mutants
// ============================================================================

TEST_SUITE("vortex deep") {
    TEST_CASE("middle zone lerps speedinner → speedouter") {
        json j  = { { "name", "vortex" },
                    { "controlpoint", 0 }, { "axis", "0 0 1" },
                    { "speedinner", 200.0 }, { "speedouter", 0.0 },
                    { "distanceinner", 50.0 }, { "distanceouter", 150.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(75, 0, 0);
        auto out = runOpSingle(op, p, 0.1);
        CHECK(std::abs(out.velocity.y()) > 0.0f);
    }

    TEST_CASE("outer-zone speed = speedouter") {
        json j  = { { "name", "vortex" },
                    { "controlpoint", 0 }, { "axis", "0 0 1" },
                    { "speedinner", 0.0 }, { "speedouter", 200.0 },
                    { "distanceinner", 50.0 }, { "distanceouter", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(150, 0, 0);
        auto out = runOpSingle(op, p, 0.1);
        CHECK(std::abs(out.velocity.y()) > 0.0f);
    }

    TEST_CASE("offset shifts the CP origin") {
        json j  = { { "name", "vortex" },
                    { "controlpoint", 0 }, { "axis", "0 0 1" },
                    { "offset", "100 0 0" },
                    { "speedinner", 100.0 }, { "speedouter", 0.0 },
                    { "distanceinner", 50.0 }, { "distanceouter", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(110, 0, 0);
        auto out = runOpSingle(op, p, 0.1);
        CHECK(std::abs(out.velocity.y()) > 0.0f);
    }

    TEST_CASE("inner zone (distance < inner) applies speedinner branch") {
        json j  = { { "name", "vortex" },
                    { "controlpoint", 0 }, { "axis", "0 0 1" },
                    { "speedinner", 100.0 }, { "speedouter", 0.0 },
                    { "distanceinner", 50.0 }, { "distanceouter", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(20, 0, 0); // strictly < distanceinner
        auto out = runOpSingle(op, p, 0.1);
        // Both `dis_mid<0||distance<inner` (true) AND middle/outer skipped.
        // Speed is fully speedinner.
        CHECK(std::abs(out.velocity.y()) > 0.0f);
    }

    TEST_CASE("outer zone (distance > outer) applies speedouter only") {
        json j  = { { "name", "vortex" },
                    { "controlpoint", 0 }, { "axis", "0 0 1" },
                    { "speedinner", 0.0 }, { "speedouter", 50.0 },
                    { "distanceinner", 30.0 }, { "distanceouter", 50.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(100, 0, 0); // strictly > distanceouter
        auto out = runOpSingle(op, p, 0.1);
        CHECK(std::abs(out.velocity.y()) > 0.0f);
    }
}

// ============================================================================
// oscillatealpha / oscillatesize / oscillateposition — surviving 10 mutants
// ============================================================================

TEST_SUITE("oscillatealpha/size/position deep") {
    TEST_CASE("oscillatealpha modifies alpha within [0, 1]") {
        json j  = { { "name", "oscillatealpha" },
                    { "frequencymin", 1.0 }, { "frequencymax", 1.0 },
                    { "scalemin", 0.5 }, { "scalemax", 1.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.alpha = 1.0f;
        auto out = runOpSingle(op, p);
        CHECK(out.alpha >= 0.0f);
        CHECK(out.alpha <= 1.0f);
    }

    TEST_CASE("oscillatesize multiplies size by lerp(scalemin, scalemax)") {
        json j  = { { "name", "oscillatesize" },
                    { "frequencymin", 1.0 }, { "frequencymax", 1.0 },
                    { "scalemin", 0.5 }, { "scalemax", 1.5 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.size = 10.0f;
        auto out = runOpSingle(op, p);
        CHECK(out.size >= 4.5f);
        CHECK(out.size <= 16.0f);
    }

    TEST_CASE("oscillateposition mask=0 axes stay zero") {
        json j  = { { "name", "oscillateposition" },
                    { "frequencymin", 1.0 }, { "frequencymax", 1.0 },
                    { "scalemin", 1.0 }, { "scalemax", 1.0 },
                    { "mask", "0 0 1" } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(0, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.position.x() == doctest::Approx(0.0f));
        CHECK(out.position.y() == doctest::Approx(0.0f));
    }
}

// ============================================================================
// turbulence — surviving 7 mutants
// ============================================================================

TEST_SUITE("turbulence deep") {
    TEST_CASE("incoherent path with mask=1 1 0 zeros only z motion") {
        json j  = { { "name", "turbulence" },
                    { "scale", 100.0 }, { "timescale", 100.0 },
                    { "speedmin", 100.0 }, { "speedmax", 100.0 },
                    { "mask", "1 1 0" } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(1, 1, 1);
        auto out = runOpSingle(op, p, 0.5);
        CHECK(out.position.z() == doctest::Approx(1.0f).epsilon(0.05));
    }

    TEST_CASE("coherent path: zero scale → finite velocity") {
        json j  = { { "name", "turbulence" },
                    { "scale", 0.0 }, { "timescale", 0.5 },
                    { "speedmin", 100.0 }, { "speedmax", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(1, 2, 3);
        auto out = runOpSingle(op, p, 0.016);
        CHECK(std::isfinite(out.velocity.x()));
    }
}

// ============================================================================
// turbulentvelocityrandom — surviving 10 mutants
// ============================================================================

TEST_SUITE("turbulentvelocityrandom deep") {
    TEST_CASE("speed within [speedmin, speedmax] after init (no scale rotation)") {
        json j = { { "name", "turbulentvelocityrandom" },
                   { "speedmin", 50.0 }, { "speedmax", 50.0 },
                   { "scale", 0.0 },
                   { "timescale", 1.0 },
                   { "offset", 0.0 } };
        auto init = WPParticleParser::genParticleInitOp(j);
        for (int i = 0; i < 10; i++) {
            Particle p = makeParticle();
            init(p, 0.5);
            CHECK(p.velocity.norm() == doctest::Approx(50.0f).epsilon(0.05));
        }
    }

    TEST_CASE("offset rotation applied after limit") {
        json j = { { "name", "turbulentvelocityrandom" },
                   { "speedmin", 1.0 }, { "speedmax", 1.0 },
                   { "scale", 0.0 },
                   { "timescale", 1.0 },
                   { "offset", 1.5707963 },
                   { "right", "0 0 1" } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle();
        init(p, 0.5);
        CHECK(p.velocity.norm() == doctest::Approx(1.0f).epsilon(0.05));
    }

    TEST_CASE("scale > 0 applied to direction limit") {
        json j = { { "name", "turbulentvelocityrandom" },
                   { "speedmin", 1.0 }, { "speedmax", 1.0 },
                   { "scale", 0.5 },
                   { "timescale", 1.0 },
                   { "offset", 0.0 },
                   { "forward", "0 1 0" },
                   { "right", "0 0 1" } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle();
        init(p, 0.5);
        CHECK(std::isfinite(p.velocity.norm()));
    }

    TEST_CASE("duration loop runs until duration <= 0.01") {
        json j = { { "name", "turbulentvelocityrandom" },
                   { "speedmin", 1.0 }, { "speedmax", 1.0 },
                   { "scale", 0.0 },
                   { "timescale", 1.0 } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle();
        init(p, 0.05);
        CHECK(std::isfinite(p.velocity.norm()));
    }
}

// ============================================================================
// movement drag — surviving 2 mutants
// ============================================================================

TEST_SUITE("movement drag deep") {
    TEST_CASE("drag > 0: velocity multiplied by (1 - drag*dt)") {
        json j = { { "name", "movement" }, { "drag", 1.0f } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.velocity = Eigen::Vector3f(100, 0, 0);
        auto out = runOpSingle(op, p, 0.1);
        // factor = max(0, 1 - 0.1) = 0.9 → 100 * 0.9 = 90.
        CHECK(out.velocity.x() == doctest::Approx(90.0f).epsilon(0.05));
    }

    TEST_CASE("drag = 0: velocity unchanged (drag branch skipped)") {
        json j = { { "name", "movement" } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.velocity = Eigen::Vector3f(50, 0, 0);
        auto out = runOpSingle(op, p, 0.1);
        CHECK(out.velocity.x() == doctest::Approx(50.0f));
    }

    TEST_CASE("drag*dt > 1 clamps factor to 0") {
        json j = { { "name", "movement" }, { "drag", 100.0f } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.velocity = Eigen::Vector3f(50, 0, 0);
        auto out = runOpSingle(op, p, 1.0);
        CHECK(out.velocity.x() == doctest::Approx(0.0f));
    }
}

// ============================================================================
// capvelocity — surviving 3 mutants
// ============================================================================

TEST_SUITE("capvelocity deep") {
    TEST_CASE("speed <= maxspeed → unchanged") {
        json j  = { { "name", "capvelocity" }, { "maxspeed", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.velocity = Eigen::Vector3f(50, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.velocity.x() == doctest::Approx(50.0f));
    }

    TEST_CASE("speed > maxspeed → scaled to maxspeed") {
        json j  = { { "name", "capvelocity" }, { "maxspeed", 50.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.velocity = Eigen::Vector3f(200, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.velocity.norm() == doctest::Approx(50.0f).epsilon(0.05));
    }

    TEST_CASE("speed < 1e-9 (zero) → skip") {
        json j  = { { "name", "capvelocity" }, { "maxspeed", 50.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        auto out = runOpSingle(op, p);
        CHECK(out.velocity.norm() == doctest::Approx(0.0f));
    }

    TEST_CASE("dead particle skipped in capvelocity") {
        json j  = { { "name", "capvelocity" }, { "maxspeed", 50.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.lifetime = 0.0f;
        p.velocity = Eigen::Vector3f(200, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.velocity.x() == doctest::Approx(200.0f));
    }
}

// ============================================================================
// lifetimerandom jitter — surviving 4 mutants
// ============================================================================

TEST_SUITE("lifetimerandom jitter deep") {
    TEST_CASE("min==max && t>0: jitter active in [0.95, 1.05]") {
        json j = { { "name", "lifetimerandom" }, { "min", 1.0 }, { "max", 1.0 } };
        auto init = WPParticleParser::genParticleInitOp(j);
        std::vector<float> seen;
        for (int i = 0; i < 100; i++) {
            Particle p = makeParticle();
            init(p, 0.0);
            seen.push_back(p.lifetime);
        }
        for (float v : seen) {
            CHECK(v >= 0.95f);
            CHECK(v <= 1.05f);
        }
        bool has_variation = false;
        for (size_t i = 1; i < seen.size(); i++) {
            if (std::abs(seen[i] - seen[0]) > 0.001f) { has_variation = true; break; }
        }
        CHECK(has_variation);
    }

    TEST_CASE("min==max && t==0: jitter NOT applied (preserves zero)") {
        json j = { { "name", "lifetimerandom" }, { "min", 0.0 }, { "max", 0.0 } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle(); init(p, 0.0);
        CHECK(p.lifetime == doctest::Approx(0.0f));
    }

    TEST_CASE("min != max: no extra ±5% jitter applied") {
        json j = { { "name", "lifetimerandom" }, { "min", 1.0 }, { "max", 2.0 } };
        auto init = WPParticleParser::genParticleInitOp(j);
        for (int i = 0; i < 30; i++) {
            Particle p = makeParticle(); init(p, 0.0);
            CHECK(p.lifetime >= 1.0f);
            CHECK(p.lifetime <= 2.0f);
        }
    }
}

// ============================================================================
// inheritcontrolpointvelocity (line 537-557) — 1 mutant
// ============================================================================

TEST_SUITE("inheritcontrolpointvelocity deep") {
    TEST_CASE("controlpoint OOB (>= cp_size): early-return") {
        std::vector<ParticleControlpoint> cps_storage(8, ParticleControlpoint {});
        for (auto& cp : cps_storage) {
            cp.velocity = Eigen::Vector3d(7, 0, 0);
            cp.resolved = Eigen::Vector3d(0, 0, 0);
        }
        std::span<const ParticleControlpoint> empty_span(cps_storage.data(), 0);
        json j = { { "name", "inheritcontrolpointvelocity" },
                   { "controlpoint", 0 },
                   { "min", 1.0 }, { "max", 1.0 } };
        auto init = WPParticleParser::genParticleInitOp(j, empty_span);
        Particle p = makeParticle();
        init(p, 0.0);
        CHECK(p.velocity.x() == doctest::Approx(0.0f));
    }

    TEST_CASE("controlpoint within range: scales CP velocity by random in [min,max]") {
        std::vector<ParticleControlpoint> cps_storage(8, ParticleControlpoint {});
        for (auto& cp : cps_storage) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        cps_storage[0].velocity = Eigen::Vector3d(10, 0, 0);
        json j = { { "name", "inheritcontrolpointvelocity" },
                   { "controlpoint", 0 },
                   { "min", 1.0 }, { "max", 1.0 } };
        auto init = WPParticleParser::genParticleInitOp(
            j, std::span<const ParticleControlpoint>(cps_storage.data(), cps_storage.size()));
        Particle p = makeParticle();
        init(p, 0.0);
        CHECK(p.velocity.x() == doctest::Approx(10.0f));
    }
}

// ============================================================================
// mapsequencearoundcontrolpoint (lines 593-627) — surviving 8 mutants
// ============================================================================

TEST_SUITE("mapsequencearoundcontrolpoint deep") {
    TEST_CASE("count=4 places particles at angle=π/2 increments") {
        std::vector<ParticleControlpoint> cps_storage(8, ParticleControlpoint {});
        for (auto& cp : cps_storage) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        json j = { { "name", "mapsequencearoundcontrolpoint" },
                   { "count", 4 }, { "controlpoint", 0 },
                   { "axis", "10 10 0" } };
        auto init = WPParticleParser::genParticleInitOp(
            j, std::span<const ParticleControlpoint>(cps_storage.data(), cps_storage.size()));
        Particle p0 = makeParticle(); init(p0, 0.0); // idx=0, angle=0 → (10,0,0)
        Particle p1 = makeParticle(); init(p1, 0.0); // idx=1, angle=π/2 → (0,10,0)
        Particle p2 = makeParticle(); init(p2, 0.0); // idx=2 → (-10,0,0)
        Particle p3 = makeParticle(); init(p3, 0.0); // idx=3 → (0,-10,0)
        CHECK(p0.position.x() == doctest::Approx(10.0f).epsilon(0.05));
        CHECK(p0.position.y() == doctest::Approx(0.0f).epsilon(0.05));
        CHECK(p1.position.x() == doctest::Approx(0.0f).epsilon(0.05));
        CHECK(p1.position.y() == doctest::Approx(10.0f).epsilon(0.05));
        CHECK(p2.position.x() == doctest::Approx(-10.0f).epsilon(0.05));
        CHECK(p3.position.y() == doctest::Approx(-10.0f).epsilon(0.05));
    }

    TEST_CASE("CP offset shifts the ring origin") {
        std::vector<ParticleControlpoint> cps_storage(8, ParticleControlpoint {});
        for (auto& cp : cps_storage) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        cps_storage[0].resolved = Eigen::Vector3d(50, 0, 0);
        json j = { { "name", "mapsequencearoundcontrolpoint" },
                   { "count", 4 }, { "controlpoint", 0 },
                   { "axis", "10 10 0" } };
        auto init = WPParticleParser::genParticleInitOp(
            j, std::span<const ParticleControlpoint>(cps_storage.data(), cps_storage.size()));
        Particle p = makeParticle();
        init(p, 0.0);
        CHECK(p.position.x() == doctest::Approx(60.0f).epsilon(0.05));
    }

    TEST_CASE("particle velocity zeroed regardless of pre-init") {
        std::vector<ParticleControlpoint> cps_storage(8, ParticleControlpoint {});
        for (auto& cp : cps_storage) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        json j = { { "name", "mapsequencearoundcontrolpoint" },
                   { "count", 4 }, { "controlpoint", 0 } };
        auto init = WPParticleParser::genParticleInitOp(
            j, std::span<const ParticleControlpoint>(cps_storage.data(), cps_storage.size()));
        Particle p = makeParticle();
        p.velocity = Eigen::Vector3f(99, 99, 99);
        init(p, 0.0);
        CHECK(p.velocity.norm() == doctest::Approx(0.0f));
    }
}

// ============================================================================
// remapinitialvalue (lines 294-329) — surviving 7 mutants
// ============================================================================

TEST_SUITE("remapinitialvalue deep") {
    TEST_CASE("output==size, operation==multiply scales size") {
        std::vector<ParticleControlpoint> cps_storage(8, ParticleControlpoint {});
        for (auto& cp : cps_storage) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        json j = { { "name", "remapinitialvalue" },
                   { "input", "distancetocontrolpoint" },
                   { "output", "size" }, { "operation", "multiply" },
                   { "inputrangemin", 0.0 }, { "inputrangemax", 100.0 },
                   { "outputrangemin", 1.0 }, { "outputrangemax", 5.0 },
                   { "inputcontrolpoint0", 0 } };
        auto init = WPParticleParser::genParticleInitOp(
            j, std::span<const ParticleControlpoint>(cps_storage.data(), cps_storage.size()));
        Particle p = makeParticle();
        p.size = 10.0f;
        p.position = Eigen::Vector3f(50, 0, 0);
        init(p, 0.0);
        CHECK(p.size == doctest::Approx(30.0f).epsilon(0.05));
    }

    TEST_CASE("output==size, no operation key → InitSize replaces") {
        std::vector<ParticleControlpoint> cps_storage(8, ParticleControlpoint {});
        for (auto& cp : cps_storage) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        json j = { { "name", "remapinitialvalue" },
                   { "input", "distancetocontrolpoint" },
                   { "output", "size" },
                   { "inputrangemin", 0.0 }, { "inputrangemax", 100.0 },
                   { "outputrangemin", 7.0 }, { "outputrangemax", 7.0 },
                   { "inputcontrolpoint0", 0 } };
        auto init = WPParticleParser::genParticleInitOp(
            j, std::span<const ParticleControlpoint>(cps_storage.data(), cps_storage.size()));
        Particle p = makeParticle();
        p.size = 10.0f;
        init(p, 0.0);
        CHECK(p.size == doctest::Approx(7.0f).epsilon(0.05));
    }

    TEST_CASE("inMax <= inMin: t = 0 (no division by zero)") {
        std::vector<ParticleControlpoint> cps_storage(8, ParticleControlpoint {});
        for (auto& cp : cps_storage) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        json j = { { "name", "remapinitialvalue" },
                   { "input", "distancetocontrolpoint" },
                   { "output", "size" }, { "operation", "set" },
                   { "inputrangemin", 100.0 }, { "inputrangemax", 100.0 },
                   { "outputrangemin", 11.0 }, { "outputrangemax", 99.0 },
                   { "inputcontrolpoint0", 0 } };
        auto init = WPParticleParser::genParticleInitOp(
            j, std::span<const ParticleControlpoint>(cps_storage.data(), cps_storage.size()));
        Particle p = makeParticle();
        p.size = 10.0f;
        init(p, 0.0);
        CHECK(p.size == doctest::Approx(11.0f).epsilon(0.05));
    }

    TEST_CASE("inputCP > 7 gets clamped to 7") {
        std::vector<ParticleControlpoint> cps_storage(8, ParticleControlpoint {});
        for (auto& cp : cps_storage) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        cps_storage[7].resolved = Eigen::Vector3d(50, 0, 0);
        json j = { { "name", "remapinitialvalue" },
                   { "input", "distancetocontrolpoint" },
                   { "output", "size" }, { "operation", "set" },
                   { "inputrangemin", 0.0 }, { "inputrangemax", 100.0 },
                   { "outputrangemin", 0.0 }, { "outputrangemax", 100.0 },
                   { "inputcontrolpoint0", 99 } };
        auto init = WPParticleParser::genParticleInitOp(
            j, std::span<const ParticleControlpoint>(cps_storage.data(), cps_storage.size()));
        Particle p = makeParticle();
        p.size = 0.0f;
        init(p, 0.0);
        CHECK(p.size == doctest::Approx(50.0f).epsilon(0.05));
    }
}

// ============================================================================
// positionoffsetrandom (lines 248-273) — surviving 5 mutants
// ============================================================================

TEST_SUITE("positionoffsetrandom deep") {
    TEST_CASE("distance == 0 → no offset") {
        json j = { { "name", "positionoffsetrandom" }, { "distance", 0.0 } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle();
        init(p, 0.0);
        CHECK(p.position.norm() == doctest::Approx(0.0f));
    }

    TEST_CASE("distance < 0 also short-circuits") {
        json j = { { "name", "positionoffsetrandom" }, { "distance", -10.0 } };
        auto init = WPParticleParser::genParticleInitOp(j);
        Particle p = makeParticle();
        init(p, 0.0);
        CHECK(p.position.norm() == doctest::Approx(0.0f));
    }

    TEST_CASE("distance > 0 with 2+ CPs: along-line component projected out") {
        std::vector<ParticleControlpoint> cps_storage(2, ParticleControlpoint {});
        cps_storage[0].resolved = Eigen::Vector3d(0, 0, 0);
        cps_storage[1].resolved = Eigen::Vector3d(100, 0, 0);
        json j = { { "name", "positionoffsetrandom" }, { "distance", 10.0 } };
        auto init = WPParticleParser::genParticleInitOp(
            j, std::span<const ParticleControlpoint>(cps_storage.data(), cps_storage.size()));
        for (int i = 0; i < 30; i++) {
            Particle p = makeParticle();
            init(p, 0.0);
            CHECK(std::abs(p.position.x()) < 0.01f);
        }
    }
}

// ============================================================================
// maintaindistancetocontrolpoint — surviving 5 mutants
// ============================================================================

TEST_SUITE("maintaindistancetocontrolpoint deep") {
    TEST_CASE("variablestrength=0: hard snap onto sphere") {
        json j  = { { "name", "maintaindistancetocontrolpoint" },
                    { "controlpoint", 0 },
                    { "distance", 100.0 }, { "variablestrength", 0.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(50, 0, 0);
        auto out = runOpSingle(op, p);
        double dist = std::sqrt((double)out.position.x()*out.position.x() +
                                (double)out.position.y()*out.position.y() +
                                (double)out.position.z()*out.position.z());
        CHECK(dist == doctest::Approx(100.0).epsilon(0.05));
    }

    TEST_CASE("variablestrength>0: soft pull (acceleration)") {
        json j  = { { "name", "maintaindistancetocontrolpoint" },
                    { "controlpoint", 0 },
                    { "distance", 100.0 }, { "variablestrength", 1.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(50, 0, 0);
        auto out = runOpSingle(op, p, 0.1);
        CHECK(out.velocity.x() > 0.0f);
    }

    TEST_CASE("particle at CP (dist<1e-6): skipped") {
        json j  = { { "name", "maintaindistancetocontrolpoint" },
                    { "controlpoint", 0 },
                    { "distance", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(0, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.position.norm() == doctest::Approx(0.0f));
    }
}

// ============================================================================
// collisionplane / sphere / box — surviving mutants
// ============================================================================

TEST_SUITE("collisionplane deep") {
    TEST_CASE("particle below plane: snapped + reflected") {
        json j  = { { "name", "collisionplane" },
                    { "controlpoint", 0 },
                    { "plane", "0 1 0 0" } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(0, -1, 0);
        p.velocity = Eigen::Vector3f(0, -10, 0);
        auto out = runOpSingle(op, p, 0.1);
        CHECK(out.position.y() == doctest::Approx(0.0f).epsilon(0.001));
        CHECK(out.velocity.y() > 0.0f);
    }

    TEST_CASE("particle above plane: untouched") {
        json j  = { { "name", "collisionplane" },
                    { "controlpoint", 0 },
                    { "plane", "0 1 0 0" } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(0, 5, 0);
        p.velocity = Eigen::Vector3f(0, -1, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.velocity.y() == doctest::Approx(-1.0f));
        CHECK(out.position.y() == doctest::Approx(5.0f));
    }

    TEST_CASE("zero plane normal falls back to (0,1,0)") {
        json j  = { { "name", "collisionplane" },
                    { "controlpoint", 0 },
                    { "plane", "0 0 0 0" } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(0, -1, 0);
        p.velocity = Eigen::Vector3f(0, -10, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.velocity.y() > 0.0f);
    }
}

TEST_SUITE("collisionsphere deep") {
    TEST_CASE("particle inside sphere: pushed to surface and reflected") {
        json j  = { { "name", "collisionsphere" },
                    { "controlpoint", 0 },
                    { "radius", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(50, 0, 0);
        // Velocity must be moving INTO the surface for ReflectVelocity to fire.
        // The sphere is convex from outside; from inside, hitting the surface
        // means moving outward (in the +radial direction).  But ReflectVelocity
        // checks v·n < 0; with n = +x (outward radial), v needs negative dot
        // which means moving inward.  An outward-moving particle is treated as
        // already exiting and is not reflected.
        p.velocity = Eigen::Vector3f(-10, 0, 0); // moving INTO the surface (toward center)
        auto out = runOpSingle(op, p);
        CHECK(out.position.x() == doctest::Approx(100.0f).epsilon(0.05));
        // Velocity reflected: was -x, normal is +x → v · n = -10 < 0 → reflect.
        CHECK(out.velocity.x() > 0.0f);
    }

    TEST_CASE("particle outside sphere: untouched") {
        json j  = { { "name", "collisionsphere" },
                    { "controlpoint", 0 },
                    { "radius", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(150, 0, 0);
        p.velocity = Eigen::Vector3f(10, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.position.x() == doctest::Approx(150.0f));
        CHECK(out.velocity.x() == doctest::Approx(10.0f));
    }

    TEST_CASE("particle exactly at center: skipped (dist<1e-9)") {
        json j  = { { "name", "collisionsphere" },
                    { "controlpoint", 0 },
                    { "radius", 100.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(0, 0, 0);
        p.velocity = Eigen::Vector3f(10, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.position.x() == doctest::Approx(0.0f));
        CHECK(out.velocity.x() == doctest::Approx(10.0f));
    }
}

TEST_SUITE("collisionbox deep") {
    TEST_CASE("particle exits +x: clamped and bounced") {
        json j  = { { "name", "collisionbox" },
                    { "controlpoint", 0 },
                    { "halfsize", "100 100 100" },
                    { "restitution", 1.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(150, 0, 0);
        p.velocity = Eigen::Vector3f(10, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.position.x() == doctest::Approx(100.0f));
        CHECK(out.velocity.x() == doctest::Approx(-10.0f));
    }

    TEST_CASE("particle exits -x: clamped and bounced") {
        json j  = { { "name", "collisionbox" },
                    { "controlpoint", 0 },
                    { "halfsize", "100 100 100" },
                    { "restitution", 1.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(-150, 0, 0);
        p.velocity = Eigen::Vector3f(-10, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.position.x() == doctest::Approx(-100.0f));
        CHECK(out.velocity.x() == doctest::Approx(10.0f));
    }

    TEST_CASE("particle exits +y but moving -y (no reflect)") {
        json j  = { { "name", "collisionbox" },
                    { "controlpoint", 0 },
                    { "halfsize", "100 100 100" },
                    { "restitution", 1.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(0, 150, 0);
        p.velocity = Eigen::Vector3f(0, -10, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.position.y() == doctest::Approx(100.0f));
        CHECK(out.velocity.y() == doctest::Approx(-10.0f));
    }

    TEST_CASE("collisionbounds aliases collisionbox") {
        json j  = { { "name", "collisionbounds" },
                    { "controlpoint", 0 },
                    { "halfsize", "50 50 50" },
                    { "restitution", 1.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(75, 0, 0);
        p.velocity = Eigen::Vector3f(5, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.position.x() == doctest::Approx(50.0f));
        CHECK(out.velocity.x() == doctest::Approx(-5.0f));
    }

    TEST_CASE("restitution scales reflected velocity") {
        json j  = { { "name", "collisionbox" },
                    { "controlpoint", 0 },
                    { "halfsize", "100 100 100" },
                    { "restitution", 0.5 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(150, 0, 0);
        p.velocity = Eigen::Vector3f(10, 0, 0);
        auto out = runOpSingle(op, p);
        CHECK(out.velocity.x() == doctest::Approx(-5.0f));
    }
}

// ============================================================================
// maintaindistancebetweencontrolpoints — surviving 4 mutants
// ============================================================================

TEST_SUITE("maintaindistancebetweencontrolpoints deep") {
    TEST_CASE("particle past cp1: clamped back along line") {
        std::vector<ParticleControlpoint> cps_storage(8, ParticleControlpoint {});
        for (auto& cp : cps_storage) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        cps_storage[1].resolved = Eigen::Vector3d(100, 0, 0);
        json j = { { "name", "maintaindistancebetweencontrolpoints" } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(150, 5, 0);
        std::vector<Particle> ps; ps.reserve(8); ps.push_back(p);
        ParticleInfo info {
            .particles     = std::span<Particle>(ps),
            .controlpoints = std::span<ParticleControlpoint>(cps_storage.data(), cps_storage.size()),
            .time          = 0.0,
            .time_pass     = 0.016,
        };
        op(info);
        CHECK(ps[0].position.x() == doctest::Approx(100.0f));
        CHECK(ps[0].position.y() == doctest::Approx(5.0f));
    }

    TEST_CASE("particle before cp0: clamped to cp0") {
        std::vector<ParticleControlpoint> cps_storage(8, ParticleControlpoint {});
        for (auto& cp : cps_storage) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        cps_storage[1].resolved = Eigen::Vector3d(100, 0, 0);
        json j = { { "name", "maintaindistancebetweencontrolpoints" } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(-50, 5, 0);
        std::vector<Particle> ps; ps.reserve(8); ps.push_back(p);
        ParticleInfo info {
            .particles     = std::span<Particle>(ps),
            .controlpoints = std::span<ParticleControlpoint>(cps_storage.data(), cps_storage.size()),
            .time          = 0.0,
            .time_pass     = 0.016,
        };
        op(info);
        CHECK(ps[0].position.x() == doctest::Approx(0.0f));
        CHECK(ps[0].position.y() == doctest::Approx(5.0f));
    }

    TEST_CASE("particle inside [0,1] range: untouched") {
        std::vector<ParticleControlpoint> cps_storage(8, ParticleControlpoint {});
        for (auto& cp : cps_storage) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        cps_storage[1].resolved = Eigen::Vector3d(100, 0, 0);
        json j = { { "name", "maintaindistancebetweencontrolpoints" } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(50, 5, 0);
        std::vector<Particle> ps; ps.reserve(8); ps.push_back(p);
        ParticleInfo info {
            .particles     = std::span<Particle>(ps),
            .controlpoints = std::span<ParticleControlpoint>(cps_storage.data(), cps_storage.size()),
            .time          = 0.0,
            .time_pass     = 0.016,
        };
        op(info);
        CHECK(ps[0].position.x() == doctest::Approx(50.0f));
        CHECK(ps[0].position.y() == doctest::Approx(5.0f));
    }

    TEST_CASE("identical CPs: early return") {
        std::vector<ParticleControlpoint> cps_storage(8, ParticleControlpoint {});
        for (auto& cp : cps_storage) { cp.resolved = Eigen::Vector3d(50, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        json j = { { "name", "maintaindistancebetweencontrolpoints" } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(99, 99, 0);
        std::vector<Particle> ps; ps.reserve(8); ps.push_back(p);
        ParticleInfo info {
            .particles     = std::span<Particle>(ps),
            .controlpoints = std::span<ParticleControlpoint>(cps_storage.data(), cps_storage.size()),
            .time          = 0.0,
            .time_pass     = 0.016,
        };
        op(info);
        CHECK(ps[0].position.x() == doctest::Approx(99.0f));
    }
}

// ============================================================================
// controlpointattract / controlpointforce — surviving mutants
// ============================================================================

TEST_SUITE("controlpointattract deep") {
    TEST_CASE("attract pulls particle toward CP+offset") {
        std::vector<ParticleControlpoint> cps_storage(8, ParticleControlpoint {});
        for (auto& cp : cps_storage) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        cps_storage[0].resolved = Eigen::Vector3d(100, 0, 0);
        json j = { { "name", "controlpointattract" },
                   { "controlpoint", 0 }, { "offset", "0 0 0" },
                   { "scale", 1000.0 }, { "threshold", 200.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(0, 0, 0);
        std::vector<Particle> ps; ps.reserve(8); ps.push_back(p);
        ParticleInfo info {
            .particles     = std::span<Particle>(ps),
            .controlpoints = std::span<ParticleControlpoint>(cps_storage.data(), cps_storage.size()),
            .time          = 0.0,
            .time_pass     = 0.1,
        };
        op(info);
        CHECK(ps[0].velocity.x() > 0.0f);
    }

    TEST_CASE("force (repel) pushes particle AWAY from CP") {
        std::vector<ParticleControlpoint> cps_storage(8, ParticleControlpoint {});
        for (auto& cp : cps_storage) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        cps_storage[0].resolved = Eigen::Vector3d(100, 0, 0);
        json j = { { "name", "controlpointforce" },
                   { "controlpoint", 0 }, { "offset", "0 0 0" },
                   { "scale", 1000.0 }, { "threshold", 200.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(0, 0, 0);
        std::vector<Particle> ps; ps.reserve(8); ps.push_back(p);
        ParticleInfo info {
            .particles     = std::span<Particle>(ps),
            .controlpoints = std::span<ParticleControlpoint>(cps_storage.data(), cps_storage.size()),
            .time          = 0.0,
            .time_pass     = 0.1,
        };
        op(info);
        CHECK(ps[0].velocity.x() < 0.0f);
    }

    TEST_CASE("distance >= threshold → particle skipped") {
        std::vector<ParticleControlpoint> cps_storage(8, ParticleControlpoint {});
        for (auto& cp : cps_storage) { cp.resolved = Eigen::Vector3d(0, 0, 0); cp.velocity = Eigen::Vector3d(0, 0, 0); }
        json j = { { "name", "controlpointattract" },
                   { "controlpoint", 0 }, { "offset", "0 0 0" },
                   { "scale", 1000.0 }, { "threshold", 50.0 } };
        auto op = WPParticleParser::genParticleOperatorOp(j, emptyOverride());
        auto p = makeParticle();
        p.position = Eigen::Vector3f(100, 0, 0);
        std::vector<Particle> ps; ps.reserve(8); ps.push_back(p);
        ParticleInfo info {
            .particles     = std::span<Particle>(ps),
            .controlpoints = std::span<ParticleControlpoint>(cps_storage.data(), cps_storage.size()),
            .time          = 0.0,
            .time_pass     = 0.1,
        };
        op(info);
        CHECK(ps[0].velocity.x() == doctest::Approx(0.0f));
    }
}
